一、常规优化手段
白屏 -> 首次渲染
- loading:
- 在页面最前面加loading相关的html和css
- 结合html-webpack-plugin插入loading(html+css)
- prerender-spa-plugin(`暂记`)
- 内联CSS(`不考虑缓存策略的话`)
- 骨架屏<br/>
Q: css文件的加载阻塞骨架屏的渲染<BR/>
A:使用preload,伪代码如下:

缓存策略
精简打包的代码
动态polyfill
- polyfill.io:根据浏览器
User-Agent头,判断其支持的特性,返回合适的polyfill。
webpack相关优化
Tree Shaking:- 关闭babel的模块处理:
modules: false;pachage.json配置sideEffects。 - 对于lodash、underscore这样的工具库来说尤其重要,开启了这个特性后:
import { capitalize } from 'lodash-es';- 效果
import {a} from xx 转换为 import {a} from 'xx/a'- 原理
结果treeShaking后:// a.js export function a() {} // b.js export function b(){} // package/index.js import a from './a' import b from './b' export { a, b } // app.js import {a} from 'package' console.log(a)配合 webpack 的// a.js export function a() {} // b.js 不再导出 function b(){} function b() {} // package/index.js 不再导出 b 模块 import a from './a' import b from './b' export { a } // app.js import {a} from 'package' console.log(a)scope hoisting和uglify之后,b 模块的痕迹会被完全抹杀掉。 - 副作用
如果 b 模块中添加了一些副作用,比如一个简单的 log:处理后 b 模块内容变成了:// b.js export function b(v) { reutrn v } console.log(b(1))注意:b文件中保留了console的代码。// b.js console.log(function (v){return v}(1)) - sideEffects
设置为false,即不管它是否真的有副作用,只要它没有被引用到,整个
模块/包都会被完整的移除。(上面的b文件被移除) - 注意事项:
shim或者polyfill慎用!
不是导出使用的模块需声明:"sideEffects": [ "*.css", "src/javascript/base/da.js" ]
- 关闭babel的模块处理:
splitChunksimport()
可交互 -> 内容加载完成
懒加载- 监听
scroll事件 Intersection Observer获取元素可见性
- 监听
placeholder(提前占位)
和骨架屏不同。其解决的问题:文本图片加载完前后,由于高度被撑开,导致闪屏的现象。三方组件:react-placeholder、react-hold
直出HTML(同构)优化 (缓存时注意拉取CGI接口的参数处理)
- 方案
接口动静分离 & Redis缓存(node层缓存html)- 静态数据在node层获取然后渲染;动态数据前端拉取并渲染

- 静态数据在node层获取然后渲染;动态数据前端拉取并渲染
PWA直出优化(前端缓存html)
- 同构直出的容灾策略
- 前端渲染和直出页面的访问路径不同
- 直出出错时,转发到前端渲染路径
PWA
- 核心:
Web App Manifest,Service Worker,Push API & Notification API,App Shell & App Skeleton
Service Worker
- 基于HTTPS
- 大部分API都是基于
promise-based - 运行在独立的worker进程(
webworker) - 离线缓存
- 弱网快速访问
- 使用lighthouse测试页面性能,根据评估结果,针对性优化。
- 错误监控 / 数据统计
- 一般监听
load事件注册 - 作用域
//只会对topics/下面的路径进行优化 navigator.serviceWorker.register('/topics/sw.js');
-
生命周期
- 注册
指定serviceworkerJS文件的位置,加载解析执行;load事件中注册
- 安装
将指定的静态资源进行离线缓存
- 激活
对旧缓存做删除等处理;接管控制权

- 注册
-
SW更新机制
背景:
SW没有自动更新的逻辑,它需要在页面加载(一次跳转)之后才会去请求sw.js
解决:由于浏览器判断sw.js是否更新是通过字节方式,因此修改cacheName会重新触发install并缓存资源。此外,在activate事件中,我们需要检查cacheName是否变化,如果变化则表示有了新的缓存资源,原有缓存需要删除。- 新的
SW.js文件下载,并触发install事件。 - 此时,旧的
SW还在工作,新的SW进入waiting状态(此时两个SW同时存在,旧的SW掌管当前页面)。- 等到下一次页面跳转(
二次跳转)才能展示最新的页面。 - install中缓存资源后,
self.skipWaiting()://caches是全局变量 self.addEventListener('install',e =>{ e.waitUntil( caches.open(cacheStorageKey) .then(cache => cache.addAll(cacheList)) .then(() => self.skipWaiting()) ) }) - 【workbox另一种思路】因为新的SW会进入
waiting状态,所以在waiting阶段,采用一定的策略来进行页面的刷新:如弹窗提示用户是否刷新,若刷新则调用wb.messageSW({type: 'SKIP_WAITING'});触发message事件。【webWorker】main.js中做相关逻辑处理,通过postMessage传递消息控制worker:self.addEventListener('message', (event) => { if (event.data && event.data.type === 'SKIP_WAITING') { // the new v2 Service Worker will immediately kill the old v1 activated Service Worker once the v2 Service Worker installs. self.skipWaiting(); } });
- 等到下一次页面跳转(
- 一旦新的SW接管,则会触发
activate事件;可以在此处对旧缓存做删除等处理;不重刷,接管控制权:self.clients.claim()self.addEventListener('activate',function(e){ e.waitUntil( //获取所有cache名称 caches.keys().then(cacheNames => { return Promise.all( // 获取所有不同于当前版本名称cache下的内容 cacheNames.filter(cacheNames => { return cacheNames !== cacheStorageKey }).map(cacheNames => { return caches.delete(cacheNames) }) ) }).then(() => { //直接接管当前页面的权限 return self.clients.claim() }) ) })
- 新的
-
注意:
- 设置
sw.js和manifest.json静态资源的缓存策略:不缓存,必须校验 - 冷启动,预加载
- 只缓存重要的页面如主页,链接,最近的文章等
- 不缓存图片,视频和大的文件
- 定期清除旧的缓存文件
- 提供一个”缓存到本地”的按钮,以便用户可以自行选择
- 降级方案:增加降级开关
if('serviceWorker' in navigator) { fetch('./cas').then(() => { if(降级) { //注销掉所有sw unregister(); }else { //注册 register(); } }) }
- 设置
-
首次启动优化
背景:首次加载时没有资源,所以会走线上,等于没优化
方案:构建时,把整个项目用到的资源输出到一个list,然后inline到sw.js。当sw install,就会把这个list的资源全部请求进行缓存。这样做的结果是,无论用户第一次进入我们站点的哪个页面,都会把整个站点所有的资源都加载回来并缓存。
Redux 与 IndexDB 结合
二、资源预加载(静态 & 动态)
涉及内容:
- link相关(rel、media)
- defer、async
- 缓存(4种缓存、缓存策略、ServiceWork)
- 优化网络(H2 Push、Preload/Prefetch、域名拆分)
-推送JSON/json内联,加速首页渲染
- 浏览器中各资源加载的优先级
前言
-
背景
当我们需要某些网络资源时,加载和执行往往耦合在一起,下载完立即执行,而加载过程是阻塞式的,延长了onload时间。因此如何在资源执行前预加载资源,减少等待网络的开销便是我们要探讨的问题。
-
常规做法
附一张不同资源浏览器优先级的图示(来源):

-
async/defer: 无阻塞加载

- defer:
DOMContentLoaded事件触发前执行
在现实当中,延迟脚本并不一定会按照顺序执行,也不一定会在DOMContentLoaded事件触发前执行,因此最好只包含一个延迟脚本 - async:加载完立即执行,无法控制执行时机和执行顺序。适用于无依赖的外部独立资源
不足:仅限于脚本资源;
执行时机不可控或存在执行顺序问题,用于非关键资源。 - defer:
-
使用ajax加载资源:可以实现预加载。
不足:优先级较低,无法对首屏资源提前加载。
-
Webkit浏览器预测解析:chrome的预加载扫描器
html-preload-scanner通过扫描节点中的"src","link"等属性,找到外部连接资源后进行预加载,避免了资源加载的等待时间,同样实现了提前加载以及加载和执行分离。
原始解析做法:
采用预解析(扫描)器: 不足:
- 仅限解析HTML中收集到的外链资源,对JS异步加载的资源无法提前收集。
- 未暴露类似于Preload的onload事件。
-
Server Push 图片来源

Link: <https://example.com/other/styles.css>; rel=preload; as=style;仅预加载,不推送:
Link: <https://example.com/other/styles.css>; rel=preload; as=style;nopush目标:减少请求数量和提高页面加载速度。
特点:多页面共享push cache(动态数据json除外)
适用场景:如果不推送这个资源,浏览器就会请求这个资源。
需要注意:要确保没有发起不必要的推送,浪费流量。可以使用preload标签代替,或者在HTTP头中加nopush属性。
【如果服务器或者浏览器不支持HTTP/2,那么浏览器就会按照 preload 来处理这个头信息,预加载指定的资源文件。】不足:
Edge和Safari的支持不好,慎用- 如果浏览器不从push cache中获取资源,推送反而不利于网页性能。
- 只能推送不带请求体的GET和HEAD请求
- push cache中的资源只能
使用一次 - 不考虑客户端的缓存,始终推送。
- 只对第一次访问的用户开启服务器推送;
- 保守起见,推送原本内联的资源,这样即使多推,也比内联效果好点。
- 将资源文件的缓存状态更新至客户端的Cookie
- cookie空间有限,可以使用
Golomb-compressed sets算法生成指纹,编码为base64,然后存入Cookie - 需自行处理缓存策略
- cookie空间有限,可以使用
- 仅能推送同源资源;
push cache: 只在会话中存在,一旦会话结束就会被释放;- 即使push的是最新的资源,如果http缓存中max-age没有过期,仍然使用http缓存中的资源。(【扩展】资源依次查找缓存的顺序:
内存缓存、Service Worker缓存、Disk缓存、Push缓存) - 无load/error事件
-
【扩展】
问题1:不仅js渲染阻塞,同时js执行后可能获取一些数据(JSON),才能真正渲染完成,如何解决?
- 用于动态资源的提前推送,注意参数需固定,不带随机变量的
- 服务端渲染(直出同构)
- 内联JSON(目前有些工程使用此方法传递同步数据)
另外两种较常见的渲染方式图片来源:
preload和prefetch
-
概念
- preload:声明式的
fetch,可以强制浏览器请求资源,同时不阻塞文档onload事件。当前页面使用,尽早下载,优先级较高; - prefetch:首次渲染时不需要,之后可能需要。优先级较低,在浏览器空闲时才会下载。使用场景:比如当前页可能跳转的页面,或者条件加载的资源。
- preload:声明式的
-
特点
- preload的资源存储在
内存缓存(没有设置资源的缓存策略时)中。 - 下载但不执行
- 异步加载,不影响当前页面的渲染
- 提前加载资源,在真正使用时,直接从从缓存中读取。
- 使用场景
- 当分析当前页面用户高频点击的链接,分析提取跳转页上的资源,使用prefetch预加载。
- font字体文件的预加载
由于字体文件必须等到 CSSOM 构建完成并且作用到页面元素了才会开始加载,会导致页面字体样式闪动。而浏览器为了避免FOUT,会尽量等待字体加载完成后,再显示应用了该字体的内容,会导致加载完成前显示空白。
- preload的资源存储在
-
检测prelaod和prefetch的支持情况
let { relList } = document.createElement('link'); return relList && relList.supports && relList.supports('preload'); -
如何使用
//link标签 <link rel="preload" as="style" herf="./a.css"/> <link rel="prefetch" as="script" href="./b.js"/> //动态创建 let preLink = document.createElement('link'); preLink.rel ='prefetch'; //感觉动态创建不适合preload prelink.as = 'script'; preLink.href = './a.js'; -
as属性值
不同值表明资源类型,对应的优先级不同:
style,script,image,media,document,font。 问题: 官方说法:不带“as”属性的 preload 的优先级将会等同于异步请求。 测试:没有发请求。 -
注意事项
- 造成二次下载
- 同一资源分别使用
as='style'和as='script'预加载,会造成二次下载 - prefetch和preload同时对同一资源使用,会造成二次下载
- 实际是script脚本,但使用as='style'会造成二次下载
- preload字体时不带crossOrigin(默认指定
anonymous匿名,不带认证信息),同样会造成二次下载preload字体时即使同域也需要带crossOrigin,否则同样会造成二次下载
解释:Requests without credentials use a separate connection。来源
- 同一资源分别使用
- 没有使用preload资源,Chrome会在onload事件3s后做出警示,避免无效的优化,浪费流量。
- 【扩展】浏览器对同一域名有并行加载数限制,因此考虑域名拆分等优化。
- 造成二次下载
-
实践
twitter //head中 <link rel="preload" href="https://abs.twimg.com/k/zh-cn/init.zh-cn.3b38ddbf651139df6007.js" as="script"> //body底部 <script src="https://abs.twimg.com/k/zh-cn/init.zh-cn.3b38ddbf651139df6007.js" async></script> -
preload的polyfill
- 背景知识
//若支持preload,异步下载完不会立即执行 <link rel="preload" > //下载完立即应用到DOM树 <link rel="stylesheet" > //异步下载,只有打印的时候才会应用,不符合则不会应用,因此不会阻塞渲染 <link rel="stylesheet" media='print' >- polyfill思路 (参见loadCSS,提供了css的preload的polyfill实现)
// 1. 支持preload: //由于preload只是获取样式,不会立即应用,因此使用onload改变link的rel使其立即生效9) <link rel="preload" href="style.css" as="style" onload="this.onload = null;this.rel ='stylesheet'"> 注:设置onload=null主要是因为有些浏览器会在rel改变时再次出发load事件。 // 2. 不支持preload // 1)获取全部link let links = document.getElementsByTagName("link"); // 2)缓存每个link的media var finalMedia = link.media || "all"; // 3)改变link的rel和media(异步下载但不会应用) link.rel = "stylesheet"; link.media = "only x"; // 4)如果绑定onload事件(为了启用media) if( link.addEventListener ){ link.addEventListener( "load", enableStylesheet ); } else if( link.attachEvent ){ link.attachEvent( "onload", enableStylesheet ); } // 5)为了应对旧的浏览器不支持link的onload事件 setTimeout( enableStylesheet, 3000 ); // 6)enableStylesheet回调,将media恢复,样式立即应用 link.setAttribute( "onload", null ); link.media = finalMedia;适用于对于首页无关的样式:由于preload的资源,能够异步加载样式,因此可以避免在加载首页无关样式时阻塞初始渲染。
对于首页初始渲染中重要的样式:
1)内联 (注意,会将静态资源的缓存策略与页面的缓存策略捆绑)
2)HTTP/2的serverPush
知道了preload和prefetch的用途,那如何结合项目实践呢?由于webpack目前基本是项目必备,所以首先介绍结合webpack的使用;然后对quiklink进行简单介绍。
结合webpack的实践
1. 插件:PreloadWebpackPlugin
常用的配置如下:
new PreloadWebpackPlugin({
//preload or prefetch方式
rel: '',
/*
*即<link as='' />中的as,表明资源类型,不同的类型决定了不同的执行优先级
*比如:script的优先级大于style
*/
as: '',
//排除的html页面集合,即只关联要配置的页面
excludeHtmlNames: [],
//所关联页面需要使用preload或prefetch的资源
include: []
})
其中include的两种使用:
- 根据chunk类型进行处理:
- asyncChunks:
import()动态导入的模块。可以使用prefetch方式异步加载模块; - initial:初始化需要的模块;
- allChunks:处理所有模块(asyncChunks & initial)。
- asyncChunks:
- 对已知命名的chunk,可以更精确的使用数组的方式配置需要使用的chunk
```
include: ['vendor', 'index']
```
-
注意事项
- 需要结合
HtmlWebpackPlugin插件使用 - 必须放到
HtmlWebpackPlugin后面,因为PreloadWebpackPlugin需要使用其提供的hook钩子将构造的<link>插入html中:
plugins: [ new HtmlWebpackPlugin(), new PreloadWebpackPlugin() ] - 需要结合
-
使用效果 对某个页面中include的资源,最终会在对应页面head中插入link标签:
<link as="script" href="/common.js" rel="preload"> <link as="script" href="/asyncChunk.js" rel="prefetch">当真正使用时,由于已经下载到本地,直接读取执行,性能得到较大的提升。
2. 结合import()
好处:拆分chunk,减少首屏js体积。
如果工程没有使用HtmlWebpackPlugin,可以对动态导入的资源做如下处理:
import(/* webpackPrefetch: true */)
import(/* webpackPreload: true */)
【版本限制】需webpack v4.6.0+ 才支持预取和预加载。本地测试后,发现prefetch可用,preload无效(有成功的烦请告知)。
quiklink
-
工作原理 通过获取页面中
a标签的href,试图更快的加载接下来可能要访问的页面。-
IntersectionObserver(交叉观察器): 检测当前视口的
linkslet target= document.getElementById('a'); io = new IntersectionObserver( entries => {}, { threshold: [1] //交叉区域为1时会触发callback } ); io.observe(target);【备注】常规的主要是通过
getBoundingClientRect()获取元素在视口中的详细位置,来实现滚动加载以及吸附等功能。 -
requestIdleCallback:等到浏览器空闲时
【备注】注意其和
requestAnimationFrame的区别 -
检查当前的网络环境:
navigator.connection.effectiveType //4G、2G... -
prefetch缓存的待下载的url
小巧的js库,使用了如上4个特性,每一个都值得细细品味。
-
-
工作流程:
- 浏览器空闲时,获取页面所有a标签的链接link
- 使用IntersectionObserver监听link
- 在视口区的link,使用prefetch下载
-
判断当前网络状况,若使用的是2G或者开启了省流模式(
data-saver),则不做处理data-saver: The user may enable such preference, if made available by the user agent, due to high data transfer costs, slow connection speeds, or other reasons.
题外话:prefetch有点偷流量的意思,我想看什么才消耗对应资源产生的流量,而prefetch擅自为我做主,偷偷下载很多我可能并不需要的资源(在早前流量特贵的时候这么做,估计会被打死...)。
-
三种下载资源的方式:
fetch、xhr、<link rel=prefetch href="" />
-
- 在视口区的link,使用prefetch下载
- 使用IntersectionObserver监听link
- 浏览器空闲时,获取页面所有a标签的链接link
-
使用说明
- 只支持prefetch
- a标签获取links
- 最佳实践是对后续可能访问的页面的提前下载,后续真正访问时,直接从本地获取执行
总结
综合来看,PreloadWebpackPlugin更适合对chunk而非html文件的处理;而quikLink更适合博客类的网站,或者服务端渲染的页面,这样才能实现"秒开"的预期效果。

欢迎关注公众号,不定时更新哦~
参考文献
- preload-webpack-plugin
- quicklink
- IntersectionObserver
- Preload, Prefetch And Priorities in Chrome
- loadCSS
- A Tale of Four Caches
- React 16 加载性能优化指南
- PWA直出
- 亿万级访问量下的前端同构直出实践
【欢迎留言】本文是否对你有帮助,亦或有所
遗漏笔误等,烦请告知。

