仅需2行代码,就能实现上图中的优化效果,让JS文件的加载耗时从1.4秒减少到0.4秒,大幅减少951ms(-67%),代码实现也非常简单方便,一起学起来吧~
资源优先级提示:预取回 Prefetch,预加载 Preload 和预连接 Preconnect
资源优先级提示是浏览器平台为控制资源加载时机而设计的一系列API,主要包括:
1. 四类资源优先级提示
1. 预取回 Prefetch
用于提示浏览器在CPU和网络带宽空闲时,预先下载指定URL的JS,图片等各类资源,存储到浏览器本地缓存中,从而减少该资源文件后续加载的耗时,优化用户体验。
具体使用方式是将link标签的rel属性设为prefetch,并将href属性设为目标资源URL,例如 <link rel="prefetch" href="https://github.com/JuniorTour/juniortour.js" />。
该标签插入DOM后,将触发一次href属性值对应URL的请求,并将响应保存到本地的prefetch cache中,同时不会解析、运行该资源。
可以预取回的资源有很多:JS、CSS、各种格式的图片、各种格式的音频、WASM文件、字体文件、甚至HTML文档本身都可实施 prefetch,预先缓存。
命中预取回缓存的请求,在开发者工具中的Network标签中的Size列,会有独特的(prefetch cache)标记:
crossorigin属性是浏览器同源策略的一部分,用于对link、script、img等元素指定是否允许以跨域资源共享模式加载目标资源。
默认情况下,JS脚本、图片等部分静态资源不受同源策略的限制,可以从任何跨域域名加载第三方JS文件、图片文件。
这样的规则有明显的安全风险,例如:
第三方JS文件可以访问第一方网站的错误上下文,从而获取内部信息。
第三方JS文件、图片文件的源服务器可以在请求过程中通过SSL握手验证、cookies等手段获取用户信息。
为了缓解这些安全风险,浏览器引入了可用于script、img和link标签的crossorigin属性,对于这些标签加载的资源:
没有crossorigin属性,就无法获取JavaScript的错误上下文。
将crossorigin设置为"anonymous",可以访问JavaScript的错误上下文,但在请求过程中的SSL握手阶段不会携带cookies或其他用户凭据。
将crossorigin设置为"use-credentials",既可以访问JavaScript的错误上下文,也可以在请求过程中的SSL握手阶段时携带cookies或用户凭据。
此外,Chrome浏览器的HTTP缓存以及相应的Prefetch、Preconnect资源优先级提示也会受到crossorigin属性的影响。
对于跨域资源,则其资源优先级提示也需要设置为跨域,即crossorigin="anonymous",例如:<link rel="prefetch" href="https://github.com/JuniorTour/juniortour.js" crossorigin="anonymous" />
资源是否跨域,可以依据浏览器自动附带的Sec-Fetch-Mode请求头判断:
值为no-cors,表示当前资源加载的模式并非跨域资源共享模式。其对应的资源优先级提示不需要设置为跨域crossorigin="anonymous"。
值为cors,表示当前资源加载的模式是跨域资源共享模式。其对应的资源优先级提示需要设置为跨域crossorigin="anonymous"。
2. 预加载 Preload
与预取回不同,预加载用于提高当前页面中资源加载的优先级,确保关键资源优先加载完成。
预加载最常见的用法是用于字体文件,减少因字体加载较慢导致的文字字体闪烁变化。例如:<link rel="preload" as="font" href="/main.woff" />
应用了preload提示的资源,通常会以较高的优先级率先在网页中加载,例如下图中的nato-sans.woff2请求,Priority列的值为High,加载顺序仅次于Document本身。
as属性是必填属性,是link标签带有rel="preload"属性时,确定不同类型资源优先级的依据。
3. 预连接 Preconnect
用于提前与目标域名握手,完成DNS寻址,并建立TCP和TLS链接。
具体使用方式是将link标签的rel属性设为preconnect,并将href属性设为目标域名,例如 <link rel="preconnect" href="https://github.com" />。
优化效果是通过提前完成DNS寻址、建立TCP链接和完成TLS握手,从而减少后续访问目标域名时的连接耗时,改善用户体验。
注意!强烈建议只对重要域名进行Preconnect优化,数量不要超过 6 个。
因为Preconnect生效后,会与目标域名的保持至少10秒钟的网络连接,占用设备的网络和内存资源,甚至阻碍其他资源的加载。
4. DNS预取回 DNS-Prefetch
与上文的预取回Prefetch不同,DNS预取回用于对目标域名提前进行DNS寻址,取回并缓存域名对应的IP地址,而非像预取回Prefetch那样缓存文件资源。
具体使用方式是将link标签的rel属性设为dns-prefetch,并将href属性值设为目标域名,例如 <link rel="dns-prefetch" href="https://github.com" />。
优化效果是通过提前解析出目标域名的IP地址,从而减少后续从目标域名加载资源的耗时,加快页面加载速度,改善用户体验。
通常来说,解析DNS的耗时往往有几十甚至几百毫秒,对资源加载耗时有直接影响。
DNS预取回的能力与预连接Preconnect有所重合,以往因为dns-prefetch的浏览器兼容性略好于preconnect,往往两者一同使用。 但近年来,IE被废弃,用户大都已更新到现代浏览器,兼容性不再重要,单独使用preconnect即可替代dns-prefetch。
例如,我们的静态资源部署在域名为static.zhihu.com的CDN上,那么添加如下2行HTML代码:
<link rel="preconnect" href="static.zhihu.com" />
<link rel="dns-prefetch" href="static.zhihu.com" />
就能观察到CDN上的JS、CSS等资源加载耗时大幅减少,产生了显著的优化效果:
5. 四类资源优先级提示对比
在2022年初,Chrome 102 新增了fetch-priority属性,可用来更精细地控制资源加载的优先级,目前仍处于实验阶段,未来可能会更加完善,示例如下:
<img src="important.jpg" fetchpriority="high">
<img src="small-avatar.jpg" fetchpriority="low">
<script src="low-priority.js" fetchpriority="low"></script>
// 只对 preload link 标签生效
<link href="main.css" rel="preload" as="image" fetchpriority="high">
2. 示例:快速增加资源优先级提示
笔者基于多年实践,制作了一套方便实用的资源优先级提示生成工具,目前已发布为 NPM 包:resource-hint-generator,支持根据构建产物,自动生成资源优先级提示标记。
下面我们以本书配套的fe-optimization-demo项目为例,演示如何接入该库,为我们的前端项目方便快捷地增加各类资源优先级提示。
1. 安装resource-hint-generator,并添加运行命令及参数
npm install resource-hint-generator --save-dev
并新建配置文件resource-hint-generator-config.js到项目根目录:
// resource-hint-generator-config.js
module.exports = {
resourcePath: `./dist`,
projectRootPath: __dirname,
resourceHintFileName: `resource-hint.js`,
includeFileTestFunc: (fileName) => {
return /(main.*js)|(main.*css)/g.test(fileName);
},
crossOriginValue: '',
publicPath: 'https://github.com/JuniorTour',
preconnectDomains: ['https://preconnect-example.com'],
};
主要参数说明:
- distPath:打包产物路径
- includeFileTestFunc:指定一个函数,返回布尔值表示,遍历distPath找到的的fileName,是否会被作为<link rel="prefetch">的href属性值
- publicPath:部署目标环境的CDN域名,用于和includeFileTestFunc、includeFileNames匹配到的文件名,拼接出<link rel="prefetch">标签的href属性值
- preconnectDomains:指定一个数组,数组中的每个字符串元素,都将产生2个href属性值为当前字符串的<link rel="preconnect">标签和<link rel="dns-prefetch">标签
2. 在项目打包构建完成后,运行生成工具
- 我们的目标是在项目打包完成后,遍历产物文件,生成对应的资源优先级提示。因此我们需要在项目构建完成后,运行resource-hint-generator。
例如,我们的前端项目通过调用 npm run build 运行打包构建,那只需要在这条命令中追加运行resource-hint-generator的逻辑即可实现我们的目标。
- 具体做法是,在package.json的scripts中添加generate-resource-hint命令,运行resource-hint-generator,并将&& npm run generate-resource-hint补充到原来的build命令中:
// package.json
"scripts": {
"generate-resource-hint": "resource-hint-generator",
"build": "cross-env NODE_ENV=production webpack && npm run generate-resource-hint",
}
测试运行构建后,如果在打包产物文件夹(./dist)中找到了生成的resource-hint.js文件,并且其中包含我们配置的 prefetch,preconnect目标数据,就说明配置完成了。
3. 项目上线后,加载运行生成的resource-hint.js
推荐在登录页,活动页,官网首页等前端项目外页面提前加载运行resource-hint.js ,从而在项目加载时,充分利用这些提前加载的缓存。
3. 验证,量化与评估
1. 上线前验证
优化上线前,在本地开发环境或设法直接到生产环境验证优化效果必不可少。
各类资源优先级提示是否生效,可以通过开发者工具中的网络 Network 面板判断。我们主要使用 优先级列(Priority),体积列(Size)和 加载时间序标签页(Timing)判断。
2. 建立量化监控指标
基于前文介绍的优化效果,我们可以通过对比2类监控指标在优化前后的变化来评估优化效果:
1. FCP 和 LCP
JS,CSS等各类静态资源更快的加载,更多的命中本地缓存,可以显著减少页面渲染耗时,预期也能改善我们在第一章介绍的web-vitals首次内容绘制FCP,最大内容绘制LCP2项用户体验指标。
2. 缓存命中率指标
收集优化前后生产环境中用户资源请求是否命中缓存,也可以更直接地判断优化效果。
我们可以基于Performance API的entry.duration属性来实现缓存命中率指标,示例:
<!DOCTYPE html>
<html lang="zh">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Report CacheHit metric</title>
<link
rel="prefetch"
href="https://static.zhihu.com/heifetz/6116.216a26f4.7e059bd26c25b9e701c1.css"
/>
</head>
<body>
<script>
// 上报数据到 Grafana
function report(name, label) {
// ...
}
// 检查资源加载是否命中缓存
function checkResourceCacheHit() {
// 获取页面加载性能信息
const perfEntries = performance.getEntriesByType('resource');
for (const entry of perfEntries) {
// 判断资源的加载时间是否小于50毫秒
// 50ms 来自于经验总结,可以根据实际情况调整
let hitCache = entry.duration < 50;
report('cacheHiteRate', hitCache);
}
}
setTimeout(() => {
checkResourceCacheHit();
}, 3000);
</script>
</body>
</html>
将收集到的数据上报到Grafana后,加以格式化,我们就可以做出如下图的缓存命中率可视化图表:
3. 评估优化效果
首先,记录优化前状态:在优化上线前提前上线监控指标,并收集一段时间的指标数据。建议上线前持续观察7至15天,从而尽量避免来自生产环境用户的指标数据受到工作日和节假日影响所产生的异常波动。
其次,优化上线后间隔几天多次观察,并在优化上线后1至3个月后回归优化效果,确保效果稳定。
如果资源优先级提示这一优化生效,我们应该能观察到 FCP 和 LCP 有明显的改善,例如下图:
观察FCP的评分百分比可视化图,在4月30日优化上线后,评分为优的用户占比从优化前的约50%,显著提升到了90%。
再观察一段时间这一指标,如果评分优的占比都能稳定在90%,那我们就有理由判定资源优先级优化显著地提升了用户体验!
同样的,我们也可以观察缓存命中率指标来判断优化效果,例如下图:
观察上述缓存命中率可视化图,在4月30日优化上线后,缓存命中率从优化前的约40%,显著提升到了70%,同样可以佐证我们的优化产生了显著的正向收益。
原文链接:https://juejin.cn/post/7274889579076108348