基于 uni-app 的项目如果用其自带的 preload 属性,基本没有作用,经过测试,性能还会下降。因为它会把项目所有页面的所有 js 文件,都放到 html 中,并加上 preload 属性。而 preload 属性又基本是黑盒,浏览器的空闲时刻并不能十分确定。所以如果单纯增加 manifestpreload,会增加 html 的大小,又没有起到真正的预加载作用,因此性能反而会下降。

但是,预加载本身的思路是没错的,因为通过查看 uni-app 的源码,以及分析页面的加载顺序可以发现,uni-app 是将所有页面都异步加载,即使是首页。这会导致首屏资源的加载时机太靠后。假如首页的 js 文件为 home.js,则加载顺序为:

  1. index.html 加载
  2. chunk-vendors.jsindex.js (还有其他资源)并行加载
  3. home.js 加载

可以看到 home.js 的加载时间太靠后了,其实可以提前到和 index.js 同时加载。并且 uni-app 的运行时体积很大,gzip 压缩后还有 106kb,更延缓了首屏的显示。

所以优化思路有两个:

  1. 减少 chunk-vendors 的大小
  2. home.js 加载提前

对于第一个方面,可以通过拆包,也就是 split-chunks 解决。一个最近的经验是,根据木桶短板效应,耗时最久的决定加载时间,所以最好将 vendors 拆出来的几个 js 文件大小做的差不多,没有特别耗时的资源,这种情况性能最佳。当然实际情况可能复杂的多,还是要结合项目实际。

对于第二个方面,我做了一个自定义预加载的框架,核心有3个插件:

  1. 暴露打包模块的 hash 值与资源地址的对应关系
  2. 获取某个页面所需要的所有的模块的 hash
  3. 根据用户配置,在运行时插入脚本,执行预加载

下面分别说说这几个插件是干什么的。

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 的动态加载逻辑。

整体的思路就是这样,简单一句话总结就是,让首屏资源加载更提前。