Web 性能优化之 preload 和 prefetch

preload

当涉及到提高网页性能时,preload 无疑是一个强大的工具。它允许开发者提示浏览器预先加载页面上即将用到的关键资源。preload 提供了一种声明式的命令,让浏览器提前加载指定资源(加载后并不执行),在需要执行的时候再执行。

如何使用

使用 preload 的一般方法是在 HTML 文档的 <head> 部分添加带有 rel="preload" 属性的 <link> 元素,并通过 as 属性指定资源的类型。这样,浏览器可以正确地设置资源的加载优先级

link 属性:

  • rel=“preload”
  • href:指定需要被预加载资源的资源路径
  • as:指定需要被预加载资源的类型
  • type:可以包含该元素所指向资源的MIME类型
  1. preload 使用 as 属性加载的资源将会获得与资源 “type” 属性所拥有的相同的优先级。比如说,preload as=“style” 将会获得比 as=“script” 更高的优先级

  2. 不带 as 属性的 preload 的优先级将会等同于异步请求。

<head>
  <!-- 其他资源和元素 -->
  <!-- 预加载一个关键的 CSS 文件 -->
  <link rel="preload" href="important-styles.css" as="style">
  <!-- 预加载一个关键的 JavaScript 文件 -->
  <link rel="preload" href="main.js" as="script">
</head>

在上面的代码中,important-styles.cssmain.js 会被提前加载,但不会立即执行或应用。as="script" 或者 as="style" 告诉浏览器被加载资源的类型。

也可以使用 HTTP 响应头的 Link 字段创建:

Link: <https://example.com/other/styles.css>; rel=preload; as=style

脚本和预加载

你可用脚本动态创建一个 link 标签后插入到 head 元素中。例如,这里我们创建一个 HTMLLinkElement 实例,然后将其添加到 DOM 中:

const preloadLink = document.createElement("link");
preloadLink.href = "myscript.js";
preloadLink.rel = "preload";
preloadLink.as = "script";
document.head.appendChild(preloadLink);

我在 JS 中使用自定义的 preload,它跟原本的 rel="preload" 或者 preload 头部有什么不同?

preload 在标记中声明以被浏览器的 preload scanner 扫描器扫描,在 HTML 解析器获取到标签之前,preload 的声明就会被获取,这将会比自定义的 preload 更加强大。

什么时候使用

任何你想要先加载后执行,或者想要提高页面渲染性能的场景都可以使用 preload

典型用例:

  • 在单页应用中,提前加载路由文件,提高切换路由时的渲染速度。现在大型的单页应用通常会异步加载路由文件。当用户切换路由时再异步加载相应的模块存在性能问题。可以用 preload 提前加载,提升性能。
  • 提前加载字体文件。由于字体文件必须等到 CSSOM 构建完成并且作用到页面元素了才会开始加载,会导致页面字体样式闪动(FOUT,Flash of Unstyled Text)。所以要用 preload 显式告诉浏览器提前加载。假如字体文件在CSS生效之前下载完成,则可以完全消灭FOUT。

Chrome 中的资源获取优先级

下面是在 Blink 内核的 Chrome 46 及更高版本中不同资源的加载优先级情况(Patrick Meenan

Chrome46及更高版本中不同资源的加载优先级情况

Network面板下的 Priority 部分

不同的资源在浏览器渲染的不同阶段进行加载的优先级。 在这里,我们只需要关注 DevTools Priority 体现的优先级,一共分成五个级别:

  • Highest 最高
  • Hight 高
  • Medium 中等
  • Low 低
  • Lowest 最低

资源的默认优先级:

  1. html、css、font 资源优先级最高。
  2. 然后是 preload 资源、script、xhr 请求。
  3. 图片视频、音频。
  4. 最低的是 prefetch 预读取的资源。

这里只是默认优先级,实际还会根据一定的条件去进行调整。

  1. 一般来说,我们发送的 xhr 请求都是异步请求,如果是同步的会调整为最高。
  2. 对于图片来说,会根据图片是否在可见视图内来改变优先级,现代浏览器为了首屏体验会把视口可见图片优先级提升为 High
  3. 对于添加了 defer / async 属性标签的脚本的优先级会全部降为 Low。 然后,对于没有添加该属性的脚本,根据该脚本在文档中的位置是在浏览器展示的第一张图片之前还是之后,又可分为两类。在之前的(标记early**)它会被定为 High 优先级,在之后的(标记late**)会被设置为 Medium 优先级。

prefetch

prefetch 的请求的优先级是在上面几个的最后面。也就是说,在第五个级别。

prefetch 告诉浏览器那些资源是以后可能会用到的,浏览器完成当前页面的加载后空闲时间来进行下载,并将其存储在缓存中。当用户访问其中一个预取文档时,便可以快速的从浏览器缓存中得到。这里注意是“可能”会用到,也有可能不会用到,而 preload 的话就肯定会用到。

<link rel="prefetch" href="static/imgs/future-img.png">

preload 与 prefetch 选择

对于当前页面很有必要的资源使用 preload,对于可能在将来的页面中使用的资源使用 prefetch

其他

缓存行为

Chrome 有四种缓存:HTTP 缓存,内存缓存,Service Worker 缓存和 Push 缓存(HTTP2 的内容)。preloadprefetch 都被存储在 HTTP 缓存中。

当一个资源被 preload 或者 prefetch 获取后,它可以从 HTTP 缓存移动至渲染器的内存缓存中。如果资源可以被缓存(比如说存在有效的 Cache-Control 和 max-age),它被存储在 HTTP 缓存中可以被现在或将来的任务使用,如果资源不能被缓存在 HTTP 缓存中,作为代替,它被放在内存缓存中直到被使用。

这将会浪费用户的带宽吗?

用 preload 和 prefetch 情况下,如果资源不能被缓存,那么都有可能浪费一部分带宽。

没有用到的 preload 资源在 Chrome 的 console 里会在 onload 事件 3s 后发生警告。

控制台警告

原因是你可能为了改善性能使用 preload 来缓存一定的资源,但是如果没有用到,你就做了无用功。在手机上,这相当于浪费了用户的流量,所以明确你会使用到的 preload 资源。

什么情况会导致二次获取?

preload 和 prefetch 是很简单的工具,你很容易不小心二次获取

不要用 prefetch 作为 preload 的后备,它们适用于不同的场景,常常会导致不符合预期的二次获取。使用 preload 来获取当前需要任务否则使用 prefetch 来获取将来的任务,不要一起用。

prefetch 作为 preload 的后备

preload 字体不带 crossorigin 也将会二次获取! 确保你对 preload 的字体添加 crossorigin 属性,否则他会被下载两次,这个请求使用匿名的跨域模式。这个建议也适用于字体文件在相同域名下,也适用于其他域名的获取(比如说默认的异步获取)。二次下载问题相关分析

参考

MDN - preload
用 preload 预加载页面资源 - 蚂蚁集团数据体验技术团队
有一种优化,叫 preload - 360产品技术
页面资源优化之 preload、prefetch - dengyizhenfeng
译 preload,prefetch 和它们在 Chrome 之中的优先级 - guoyang134340

> cd ..