单页应用性能的最大痛点就是 bundle 体积大导致首屏时间过长。使用 webpack 的 code splitting(代码分割)功能可以将 bundle 分片,加速首屏,但之后的交互势必会受到影响,页面变得不再流畅,稍有卡顿,所以放弃了分片,但是现在发现有webpack插件解决JS文件后加载响应慢的问题,所以记了下来。
先说结论:比较理想的解决方案是code split + prefetch 方式,对于不支持prefetch的safari浏览器另做preload处理。
preload
preload 是较新的 web 标准。他可以声明式的告诉浏览器去获取某个资源,并且可以为资源设置优先级。
1 | <link rel="preload" href="font.woff2" as="font" type="font/woff2" crossorigin> |
preload 加载资源后并不会执行,可以安全的预加载 JavaScript。我们一般使用 preload 来主动通知浏览器获取本页的关键资源。现在浏览器都有 Preloader,可以尽早发现大多数基于标记语言的资源,但依然存在一些隐藏在 CSS 和 JavaScript 中的资源,例如字体文件,其为首屏关键资源又隐藏在 CSS 中。这种场景适合使用 preload 进行声明,尽早进行资源加载,避免页面渲染延迟。
preload 的更多细节可参考文章 Preload: What Is It Good For?,它适合用来预加载被隐藏的首屏关键资源。preload 的兼容性并不理想,目前只有最新版的 chrome 和 safari 才支持。
prefetch
使用 prefetch 声明的资源是对浏览器的提示,暗示该资源可能『未来』会被用到,适用于对可能跳转到的其他路由页面进行资源缓存。被 prefetch 的资源的加载时机由浏览器决定,一般来说优先级较低,会在浏览器『空闲』时进行下载。
1 | <link rel="prefetch" href="//example.com/industry-qualification-audit/js/common-main.550d4.chunk.js"> |
由于关键资源 main.js 已被切分,体积小加载快,prefetch 的资源也未发生抢占带宽的现象,实际效果还是比较符合期望的。
prefetch 的兼容性稍好,chrome、firefox、edge、android 4.4+ 都支持,但 safari、IE11-、iOS safari 始终未支持。
3. 结论
经过分析,资源加载方式简单总结如下:
首屏关键资源:优先级高,使用阻塞方式载入,若有隐藏在 CSS、JavaScript 内部的关键资源(如字体),可使用 preload 声明提前开始加载。
首屏非关键资源(第三方插件,如广告、评论、统计、分享):优先级低,若无执行顺序要求,可使用 async 进行异步加载,但应警惕 onload 事件延迟现象(很多插件和业务逻辑都依赖 onload 事件),若产生了性能问题最好在 onload 事件后手动加载。
非首屏资源(如其他路由的分片 thunk):优先级最低,可使用 prefetch 声明进行预加载。在 safari、iOS 等不支持 prefetch 的浏览器上,在 onload 事件后手动进行加载。
单页应用的分片 thunk 为非首屏资源,可以采用 prefetch + onload 手动加载的方式实现全平台的预加载。prefetch 可以使用 preload-webpack-plugin 插件自动打入,preload可以使用prefetch-polyfill-webpack-plugin,可以自动生成在 onload 事件触发时执行的 prefetch polyfill 函数,由于其身份是作为 prefetch 的补足,所以仅在 IE、safari、iOS 上执行,可选择使用 image 或者 async模式 对分片 thunk 做预加载。
1 | new HtmlWebpackPlugin({ |
经过这样细致的优化,就可以保证我们的单页应用既有快速的首屏响应时间,又能享受流畅的交互体验了。