基于 uni-app
的项目如果用其自带的 preload
属性,基本没有作用,经过测试,性能还会下降。因为它会把项目所有页面的所有 js
文件,都放到 html
中,并加上 preload
属性。而 preload
属性又基本是黑盒,浏览器的空闲时刻并不能十分确定。所以如果单纯增加 manifest
的 preload
,会增加 html
的大小,又没有起到真正的预加载作用,因此性能反而会下降。
但是,预加载本身的思路是没错的,因为通过查看 uni-app
的源码,以及分析页面的加载顺序可以发现,uni-app
是将所有页面都异步加载,即使是首页。这会导致首屏资源的加载时机太靠后。假如首页的 js
文件为 home.js
,则加载顺序为:
index.html
加载chunk-vendors.js
和index.js
(还有其他资源)并行加载home.js
加载
可以看到 home.js
的加载时间太靠后了,其实可以提前到和 index.js
同时加载。并且 uni-app
的运行时体积很大,gzip
压缩后还有 106kb
,更延缓了首屏的显示。
所以优化思路有两个:
- 减少
chunk-vendors
的大小 home.js
加载提前
对于第一个方面,可以通过拆包,也就是 split-chunks
解决。一个最近的经验是,根据木桶短板效应,耗时最久的决定加载时间,所以最好将 vendors
拆出来的几个 js
文件大小做的差不多,没有特别耗时的资源,这种情况性能最佳。当然实际情况可能复杂的多,还是要结合项目实际。
对于第二个方面,我做了一个自定义预加载的框架,核心有3个插件:
- 暴露打包模块的
hash
值与资源地址的对应关系 - 获取某个页面所需要的所有的模块的
hash
值 - 根据用户配置,在运行时插入脚本,执行预加载
下面分别说说这几个插件是干什么的。
Webpack 运行时中,会保存一份资源 hash
和真正的地址的映射关系,内部叫 jsonpScriptSrc
,这里需要将其暴露出来 (opens new window),这样才有可能做到自定义加载某个资源。
暴露出来后,可以在控制台输入下面命令查看地址:
jsonpScriptSrc('views-other-poster-poster')
// https://image-1251917893.file.myqcloud.com/static/js/views-other-poster-poster.0c9ab0d6.js
同时,uni-app
会把所有页面当成异步的组件,源码在这里 packages/webpack-uni-pages-loader/lib/platforms/h5.js
。
Vue.component('${name}', resolve=>{
const component = {
component:require.ensure([], () => resolve(require(${JSON.stringify(path)}+'${ext}')), '${name}'),
delay:__uniConfig['async'].delay,
timeout: __uniConfig['async'].timeout
}
所有 pages.json
中注册的页面,都会被上面的代码处理,经过 Webpack
一系列 loader
处理后,打包后的资源大致如下:
e.__uniConfig.__webpack_chunk_load__=n.e,
t["default"].component("views-home-hor-index",
(function(e){var t={component:Promise.all([
n.e("default~views-home-hor-index~views-owner-ingame-match-game-list-game-list"),
n.e("views-home-hor-index")
]).then(function(){return e(n("a772"))}.bind(null,n)).catch(n.oe),delay:__uniConfig["async"].delay,timeout:__uniConfig["async"].timeout};return __uniConfig["async"]["loading"]&&(t.loading={name:"SystemAsyncLoading",render:function(e){return e(__uniConfig["async"]["loading"])}}),__uniConfig["async"]["error"]&&(t.error={name:"SystemAsyncError",render:function(e){return e(__uniConfig["async"]["error"])}}),t})),
t["default"].component("views-owner-ingame-match-team-zone-team-zone",
(function(e){var t={component:Promise.all([
n.e("chunk-2d401669"),
n.e("default~views-owner-ingame-match-ai-room-ai-room~views-owner-ingame-match-league-room-league-room~vi~fde6b837")
//...
上面的代码的意思是,每个页面都会先加载1个或多个异步的资源,然后才会渲染。所以如果我们要做自定义预渲染,需要知道一个页面有哪些异步资源,这也就是为什么要有第2个插件。
可以自行搜索 uni-app
打包生成的 index-hash.js
文件,里面一定有 __uniConfig.__webpack_chunk_load__
关键词,后面跟的就是异步页面和资源的加载关系。
第3个插件比较容易理解,每个项目的每个页面所需的预加载资源是不同的,所以需要一份配置文件,以及解析配置文件的引擎,还有注入到 index.html
的动态加载逻辑。
整体的思路就是这样,简单一句话总结就是,让首屏资源加载更提前。