Skip to content

本文由 简悦 SimpRead 转码, 原文地址 mp.weixin.qq.com

大厂技术  高级前端  Node 进阶

点击上方 程序员成长指北,关注公众号

回复 1,加入高级 Node 交流群

前言

大家好,我是考拉🐨。

资源优先级优化效果示例. png

仅需 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)标记:

DEMO:output.jsbin.com/cuxerej[1]

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本身。

DEMO:output.jsbin.com/cuxerej[2]

as属性是必填属性,是link标签带有rel="preload"属性时,确定不同类型资源优先级的依据。完整可选值请参考 MDN:link attribute as[3]

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 预取回往往与预连接配合使用,能显著减少 HTTP 资源的加载耗时,例如:


优化前(Preconnect && DNS-Prefetch优化后(Preconnect && DNS-Prefetch差异
加载耗时1400 ms451 ms-949 ms (-67%)
开发者工具计时截图在 DevTool - Network - 资源 - 计时(Timing)面板中可以看到,因为Preconnect && DNS-Prefetch优化生效,连接开始阶段的耗时从 950ms 降低到了 1ms,使得资源的整体加载耗时大幅减少。
在线 DEMOjsbin.com/panorej/edi…[4]jsbin.com/haragis/edi…[5]

5. 四类资源优先级提示对比

类型优化目标示例注意事项
预取回 Prefetch- 加载优先级较低的资源 - 后续页面浏览需要加载的资源<link rel="prefetch" href="/juniortour.js" />1. Prefetch 预取回的资源并不会被立刻解析、运行:例如预取回 JS 文件时,JS 文件内的代码逻辑并不会执行,只是文件保存到了浏览器缓存中。这也是 Prefetch 与普通 link 标签(<link href="/static/main.3da2f.css" rel="stylesheet">)的核心区别。2. Prefetch 的触发时机不固定,会由浏览器相机决定,浏览器通常会在网络带宽、CPU 运算都空闲时触发下载。
预加载 Preload当前页面需要优先加载的静态资源<link rel="preload" as="font" href="/main.woff" />- 优化目标为当前页面所需资源,而非后续加载。
预连接 Preconnect- 加载优先级较低的域名 - 后续页面浏览需要连接的域名<link rel="preconnect" href="https://juniortour.net" />- 用于跨域域名,同源域名不需要 - 控制只对关键域名应用,避免数量超过 6 个
DNS 预取回 DNS-Prefetch- 后续页面浏览需要连接的域名<link rel="dns-prefetch" href="https://juniortour.net" />(同预连接 Preconnect)

在 2022 年初,Chrome 102 新增了fetch-priority属性,可用来更精细地控制资源加载的优先级,目前仍处于实验阶段,未来可能会更加完善,示例如下:

ini复制代码<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">
  1. 示例:用 resource-hint-generator[6] 为前端项目增加资源优先级提示

笔者基于多年实践,制作了一套方便实用的资源优先级提示生成工具,目前已发布为 NPM 包:resource-hint-generator[7]。

下面我们以本书配套的 fe-optimization-demo[8] 项目为例,演示如何接入该库,为我们的前端项目方便快捷地增加各类资源优先级提示。

《接入 resource-hint-generator[9]》完整示例 Merge Request 请参考:github.com/JuniorTour/…[10]

1. 安装依赖,并添加运行命令及参数

css
复制代码
npm install resource-hint-generator --save-dev

并新建配置文件resource-hint-generator-config.js到项目根目录:

javascript复制代码// resource-hint-generator-config.jsmodule.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'],};

主要参数说明:

  • resourcePath:打包产物路径

  • includeFileTestFunc:指定一个函数,返回布尔值表示,遍历distPath找到的的fileName,是否会被作为<link rel="prefetch">href属性值

  • publicPath:部署目标环境的 CDN 域名,用于和includeFileTestFuncincludeFileNames匹配到的文件名,拼接出<link rel="prefetch">标签的href属性值

  • preconnectDomain:指定一个数组,数组中的每个字符串元素,都将产生 2 个href属性值为当前字符串的<link rel="preconnect">标签和<link rel="dns-prefetch">标签

2. 在项目打包构建完成后,运行生成工具

  1. 我们的目标是在项目打包完成后,遍历产物文件,生成对应的资源优先级提示。因此我们需要在项目构建完成后,运行resource-hint-generator

例如,我们的前端项目通过调用 npm run build 运行打包构建,那只需要在这条命令中追加运行resource-hint-generator的逻辑即可实现我们的目标。

  1. 具体做法是,在package.jsonscripts中添加generate-resource-hint命令,运行resource-hint-generator,并将&& npm run generate-resource-hint补充到原来的build命令中:
json复制代码// 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文件,并且其中包含我们配置的 prefetchpreconnect目标数据,就说明配置完成。

3. 项目上线后,加载运行生成的resource-hint.js

推荐在登录页,活动页,官网首页等前端项目外页面提前加载运行resource-hint.js ,从而在项目加载时,充分利用这些提前加载的缓存。

《接入 resource-hint-generator[11]》完整示例 Merge Request 请参考:github.com/JuniorTour/…[12]

  1. 验证,量化与评估

1. 上线前验证

优化上线前,在本地开发环境或设法直接到生产环境验证优化效果必不可少。

各类资源优先级提示是否生效,可以通过开发者工具中的网络 Network 面板判断。我们主要使用 优先级列(Priority),体积列(Size)和 加载时间序标签页(Timing)判断。

类型验证测试方式生效依据
预取回 Prefetch在页面中添加 prefetch 目标资源的 link 标签,检查Network面板。例如,在生产环境中的页面上,打开浏览器控制台直接运行下列代码验证测试: function addLinkTag(src, { rel, crossoriginVal }) { const tag = document.createElement('link'); tag.rel = rel; tag.href = src; // enable crossorigin so that prefetch cache works for // Cross Origin Isolation status. if (crossoriginVal !== undefined) { tag.setAttribute('crossorigin', crossoriginVal); } const head = document.querySelector('head'); if (head && head.appendChild) { head.appendChild(tag); } } addLinkTag('https://your-domain.com/static/main.h1712oidw.js', { rel: 'preferch', });优化目标资源加载后在Network面板中的Size列,值为独特的(prefetch cache)标记,且加载耗时极短(小于 50 毫秒):不生效解决方案 - 检查请求资源是否为跨域资源,添加对应的crossorigin属性值 - 确认 prefetch 请求和优化目标资源请求的先后顺序,确保 prefetch 请求率先完成。
预加载 Preloadpreload往往需要在较早的时机插入 DOM,从而确保资源优先加载,如果在浏览器控制台执行脚本时机太晚,也可以考虑用Charles等代理工具拦截、替换响应体,插入 preload 标签优化目标请求的Priority列的值为High,且加载时间较早,仅次于 Document 本身:
预连接 Preconnect 和 DNS 预取回 DNS-Prefetch也可通过浏览器控制台直接运行下列代码验证测试: addLinkTag('https://your-domain.com', { rel: 'preconnect', }); addLinkTag('https://your-domain.com', { rel: 'dns-prefetch', });在浏览器开发者工具中的 Network - 资源 - 计时(Timing)面板中可以看到,如果Preconnect && DNS-Prefetch优化生效,目标优化资源连接开始阶段的耗时会大幅减少到接近 1ms,例如:

2. 建立量化监控指标

3. 评估优化效果

基于前文介绍的优化效果,我们可以通过对比 2 类监控指标在优化前后的变化来评估优化效果:

1. FCP 和 LCP

JS,CSS 等各类静态资源更快的加载,更多的命中本地缓存,可以显著减少页面渲染耗时,预期也能改善我们在第一章介绍的web-vitals首次内容绘制FCP,最大内容绘制LCP2 项用户体验指标。

建立web-vitals用户体验监控指标的内容详见:前端优化数据量化必备神器:Grafana《现代前端工程体验优化》第一章 数据驱动 第二节 用户体验数据收集与可视化 [13]

2. 缓存命中率指标

此外收集优化前后生产环境中用户资源请求是否命中缓存,也可以更直接地来判断优化效果。

我们可以基于Performance APIentry.duration属性来实现缓存命中率指标,示例:

xml复制代码<!DOCTYPE html><html lang="zh">  <head>    <meta charset="UTF-8" />    <meta     />  </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 后,加以格式化,我们就可以做出如下图的缓存命中率可视化图表:

4. 判断优化效果

首先,记录优化前状态:在优化上线前提前上线监控指标,并收集一段时间的指标数据。建议上线前持续观察 7 至 15 天,从而尽量避免来自用户的指标数据受到工作日和节假日的影响,而产生异常波动。

Grafana Cloud 的免费用户自带 14 天数据保存时长,超过这一时长的数据会被删除。

其次,优化上线后间隔几天多次观察,并在优化上线后 1 至 3 个月后回归优化效果,确保效果稳定。

如果资源优先级提示这一优化生效,我们应该能观察到 FCP 和 LCP 有明显的改善,例如下图:

观察 FCP 的评分百分比可视化图,在 4 月 30 日优化上线后,评分为优的用户占比从优化前的约 50%,显著提升到了 90%。

再观察一段时间这一指标,如果评分优的占比都能稳定在 90%,那我们就有理由判定资源优先级优化显著地提升了用户体验!

同样的,我们也可以观察缓存命中率指标来判断优化效果,例如下图:

观察缓存命中率可视化图,在 4 月 30 日优化上线后,缓存命中率从优化前的约 40%,显著提升到了 70%。同样可以佐证我们的优化产生了显著的正向收益。

作者:少游_JuniorTour
链接:https://juejin.cn/post/7274889579076108348
来源:稀土掘金

结语

Node 社群

我组建了一个氛围特别好的 Node.js 社群,里面有很多 Node.js小伙伴,如果你对Node.js学习感兴趣的话(后续有计划也可以),我们可以一起进行Node.js相关的交流、学习、共建。下方加 考拉 好友回复「Node」即可。

   “分享、点赞、在看” 支持一下