Serenader

Learning by sharing

神奇的 <link> 标签

按照标准的定义,<link> 标签其实是用来表明当前页面与外部资源的关系:
The HTML External Resource Link element (<link>) specifies relationships between the current document and an external resource. —— MDN
一个最简单的例子,便是我们最熟悉的样式文件引用了:
<link href="style.css" rel="stylesheet" />
在页面 HTML 上增加这一段标签,当打开页面的时候浏览器则会帮我们加载 style.css 这个文件。href(HREF = Hypertext REFerence) 属性指定了所引用的资源的 URI 地址,rel (REL = RELationship) 则表明该资源与文档的关系。
Link 标签还有其他很有用的属性,比如,你可能不知道使用 link 来引用样式文件时,你可以根据媒介查询来动态加载不同的样式文件:
<link href="mobile.css" rel="stylesheet" media="screen and (max-width: 600px)" />
当然这篇文章并不是专门讲如何加载样式文件的,除了加载样式文件之外,link 标签对于网页资源的加载有非常大的辅助作用,如果使用得当,那么会让你的网页加载时间得到大幅提升,并且减少首屏交互时间。下面介绍几种使用 link 标签来加快网页资源加载的方法。

Preload

现如今的网页所依赖的资源并非都以 HTML 标签的形式声明在主文档里面的,很多资源都深深地藏在 CSS 、JS 文件里面,比如字体资源,比如异步的 JS 资源等。这些资源通常对于渲染页面而言是必要的,但是它并不能被浏览器提前发现,从而使得这些资源的加载时间延后,导致页面加载时间变长,也有可能会导致白屏时间变长。而 Preload 的出现则是为了解决这个问题。
Preload,顾名思义,就是用来提前加载某个资源,当浏览器解析 HTML 的时候如果碰到了一个 preload 标签,那么它则会以特定的优先级去下载这个资源。但是 preload 只是帮你提前下载,它不会帮你执行这些资源。而当你的页面真正发起这个资源的请求时,假如此时 preload 已经下载完成,那么浏览器则会直接从 preload 的下载结果里面取内容,而不会重新下载文件。这样一来,从结果来看,资源的加载时间就会变得短很多(因为已经提前帮你下载好了)。Preload 除了能帮你提前下载资源之外,它还有一个非常大的特性,它不会阻塞页面的 onload 事件
举个非常简单的例子:
<!DOCTYPE html>
<html>
<head>
  <title>Preload demo</title>
+ <link rel="preload" as="image" type="image/jpeg" href="https://avatars2.githubusercontent.com/u/6716522" >
</head>
<body>
  <p>Preload demo:</p>
  <script>
  (function() {
    window.addEventListener('load', function() {
      var image = document.createElement('img');
      image.src = 'https://avatars2.githubusercontent.com/u/6716522';
      document.body.appendChild(image);
    });
  })();
  </script>
</body>
</html>
/image/b86f8f42-055c-4d1f-a082-b5c34cab30a5/2d8eb70d-da49-40ff-821a-d7c53c647e67_Untitled.png
没有加 preload 时的请求瀑布图
/image/b86f8f42-055c-4d1f-a082-b5c34cab30a5/ff193ea7-3cdd-44f1-ba3f-9e38209dba3a_Untitled.png
加了 preload 的请求瀑布图
从上面的资源加载瀑布图可以看到,使用 preload 之后图片的加载时机瞬间提前了很多,而且资源的加载丝毫不会影响 DOMContentLoaded 事件和 Load 事件。

Preload 的使用方法

了解了 preload 的作用之后,来看看要怎么使用 preload 。与引用 CSS 类似,preload 也是使用 <link> 标签来声明的,只是 rel 属性不是 stylesheet ,而是 preload 。除此之外,还需要声明一个 as 属性,用来声明当前资源的类型。这个属性能够让浏览器:
  • 设置更加精准的资源加载优先级
  • 为资源设置正确的 Content-Security-Policy 规则
  • 为资源请求设置正确的 Accept 请求头
结合这两个属性,一个最简单的 preload 标签可以是这样子的:
<link rel="preload" as="script" href="http://example.com/test.js" />
Preload 可以加载非常多的资源类型,包括:
  • audio: 音频文件
  • document: 内嵌在 iframe 内的文档
  • embed: 内嵌在 <embed> 的资源
  • fetch: 使用 XHR 或者 fetch 加载的资源,如 ArrayBuffer,或者一个 JSON 文件
  • font: 字体资源
  • image: 图片资源
  • object: 内嵌在 <embed> 的资源
  • script: 脚本资源
  • style: 样式资源
  • track: WebVTT 文件
  • worker: web worker 文件
  • video: 视频资源

设置一个 MIME 类型

Preload 标签还可以设置一个 type 属性,它可以用来告诉浏览器当前资源的 MIME 类型。如果浏览器不支持该 MIME 类型的资源,那么则会忽略这个资源,不会去加载它。
例如,你想预加载一个 MP4 文件,你可以这样声明:
<link rel="preload" href="sintel-short.mp4" as="video" type="video/mp4">
在支持 MP4 格式的浏览器上会正常加载这个资源,而在不支持 MP4 的浏览器上则会忽略该资源。这样就可以更加精确地控制资源的加载,避免造成流量浪费的情况。

预加载跨域资源

Preload 还可以预加载跨域资源,只需要设置正确的 crossorigin 属性。例如,你想使用 preload 来预加载跨域接口,或者图片资源,你只需要在 link 标签上添加 crossorigin 属性:
<link rel="preload" href="http://exmaple.com/api/list" as="fetch" crossorigin />
此时浏览器在请求接口的时候就会自动带上相应的跨域请求头,如 Origin 请求头等。
除此之外,有一类资源比较特殊,即使是同源,使用 preload 加载也需要设置 crossorigin 属性,它就是字体资源。
<link rel="preload" href="fonts/icon.woff2" as="font" type="font/woff2" crossorigin>
如果想了解其中的原因可以查阅 W3C 的文档

其他有趣的属性

由于 Preload 本身是基于 link 标签的,因此它也拥有了诸如文章一开始提到的 media 属性,以及 onload 属性。
media 属性可以灵活地声明媒介查询,只有当浏览器满足条件时再去加载这些资源,这样你可以更加精准地控制资源的加载,避免浪费。
而 onload 属性则可以实现一些意想不到的效果。

Preload 的一些妙用

从上面我们可以了解到,preload 具有以下几个特殊的特性:
  1. 资源只下载,不执行
  2. 预加载的资源可以被重复使用
  3. 资源加载不阻塞页面 onload 事件
  4. 标签本身具有 onload 事件
它可以实现:

延迟资源的执行

从第一点就可以看到 preload 的革命性意义,它把资源的加载和执行解耦了!这意味着,资源加载完成后,我们可以延迟资源的执行,等到我们需要执行的时候再去执行它。
想象有这么一个场景,你的页面在某个时刻,比方说用户点击按钮的时候,需要执行一段第三方脚本。但是这段脚本的逻辑是立即执行的,它没有暴露一个类似 runNow() 的方法好让你手动控制它的执行时机,而你又不具备它的内容控制权。这就意味着你不能像其他脚本一样在 HTML 文件里面以常规的 <script> 标签去加载它,因为使用这种方法加载,一旦脚本加载完成就会立即执行这段脚本的逻辑,而我们希望脚本的执行时机可以手动控制。那么在 Preload 出现之前,你能做的只有当用户点击按钮的时候,手动创建一个 <script> 标签去加载它。但是加载需要时间,因此在用户点击按钮到脚本执行完毕这中间存在一个间隔,这段时间期间页面不能正常响应用户的操作,用户可能会不知所措。
而有了 preload 之后,问题就变得简单很多了。像是这种情况,我们可以在 HTML 文件里面用 <link rel="preload"> 来加载这段第三方脚本,然后当用户点击按钮的时候再手动创建 <script> 标签去加载执行它。由于使用了 preload 去预加载,因此可以认为手动创建 <script> 的时候,其加载时间可以忽略不计,此时页面就可以更快地响应用户的按钮点击,用户也不会因此感到困惑了。

优雅的异步资源加载

Preload 的特性注定了它可以用来实现资源的异步加载。利用 onload 事件,我们可以这样编写脚本的异步加载:
<link rel="preload" as="script" href="async_script.js"
onload="var script = document.createElement('script');
        script.src = this.href;
        document.body.appendChild(script);">
当资源预加载完毕之后则会立即创建一个真正的 script 标签去加载执行它,而因为 preload 的资源具备可重复利用,所以加载时间可以忽略不计。而 preload 加载资源的时候不会影响页面其他资源的加载,也不会影响 window.onload 的时机,因此非常适合用在这种场景。反观 script 标签自带的 asyncdefer 属性,它们虽然也是用来做异步加载的,但是它们会影响 window.onload 的时机。Preload 在此具有无可比拟的优越性。
除了异步加载脚本之外,也可以用来异步加载样式文件。我们都知道,<link rel="stylesheet"> 加载 CSS 文件是会阻塞标签后面 HTML 内容的解析的,在样式文件下载完成之前页面处于不可交互状态。在 preload 出现之前,想要异步加载 CSS 文件,你只能用脚本手动去请求 CSS 文件再动态创建 <style> 标签。而 preload 出现之后一切都变得非常简单,异步加载一个 CSS 文件,你只需要写一行代码:
<link href="style.css" rel="preload" onload="this.rel = 'stylesheet'" />
style.css 预加载完毕之后就会立即生效,而整个过程不会影响页面的其他资源加载,也不影响 HTML 的继续解析!
使用这种异步加载的方式,比较适合用在页面非强依赖的资源上,比如页面统计 SDK ,页面监控 SDK ,评论 SDK 等等。因为这类型的资源对于页面的关键内容渲染来说,没有任何帮助。所以可以让这些资源异步加载,使其不影响页面剩余部分的解析,这样一来有助于页面关键内容的更快展示,以此降低页面的白屏时间,和可交互时间。

Prefetch

与 preload 类似,prefetch 也是用来预加载资源。不过它与 preload 有非常大的区别。
事实上,prefetch 诞生的时间要比 preload 早得多。只不过,它的诞生是用来加速页面未来可能会用到的资源。换句话说,prefetch 是用来服务后续页面访问的,而不是当前页面访问。反过来,preload 则是用来服务当前页面访问。

Prefetch 使用方法

Prefetch 的使用方法与 preload 非常类似,只需要把 rel 属性改成 prefetch 即可:
<link rel="prefetch" href="/image.jpg" >

Prefetch 跟 Preload 区别

就像一开始说到的,prefetch 与 preload 最根本的区别是它们服务的对象不同。Preload 预加载的资源都是给当前页面使用的,如果 preload 的资源在当前页面加载完成后几秒内仍然没有用到,那么 Chrome 会在控制台输出 warning ,提醒当前 preload 的资源并没有实际用到。而 prefetch 则是给后续的页面访问使用的。
也就是说,preload 是加快当前页面的加载,而 prefetch 是用来加快第二个页面访问的速度。两者可以并存,可以视为互补关系。
在标签声明方式上,prefetch 与 preload 不同, prefetch 没有 as 属性,没有 crossorigin 属性。Prefetch 不受 CORS 跨域限制,所以你可以用来预加载其他域的资源,而不用担心被 CORS 策略限制。
除此之外,prefetch 也没有 onload 事件,这样一来你就无法判断资源究竟何时下载完毕。而且在 Firefox 浏览器上,prefetch 的资源只有在浏览器空闲的时候才会开始下载。

DNS prefetch

当我们访问网页的时候,通常情况下我们是使用域名去访问的。而浏览器在背后会默默地帮我们把域名进行 DNS 解析得到服务器 IP,这时候才能与服务器建立连接。DNS 解析有时候会耗费一定的时间,当 DNS 没有本地缓存时,它就得一层一层地往上递归查询。可以说,DNS 解析会在一定程度上影响页面的加载性能。
而 DNS prefetch 则可以让浏览器提前解析好对应域名的 IP 地址,这样一来页面在访问这些域名的资源时浏览器就无需再花时间去解析 DNS 了。这样能够加快页面的访问。
/image/b86f8f42-055c-4d1f-a082-b5c34cab30a5/a01c93fd-ff40-440f-bccb-cdc447ab6478_Untitled.png
/image/b86f8f42-055c-4d1f-a082-b5c34cab30a5/a5794134-8848-46df-88a0-e07b12b839a0_Untitled.png
上面的图片展示了有无 DNS prefetch 的请求瀑布图,可以看到,第二张图由于使用了 DNS prefetch,DNS 解析的时机被提前了。

DNS prefetch 使用方法

把 link 标签的 rel 属性改成 dns-prefetch ,然后 href 传入要解析的域名即可。
<link rel="dns-prefetch" href="//example.com">
解析 DNS 的时候只需要域名,所以 href 不需要写详细的 path 。

DNS prefetch 适用场景

DNS prefetch 适合用在提前解析第三方站点资源的 DNS。如果没有使用 DNS prefetch,那么页面在加载第三方站点资源的时候,由于域名跟原站点不同,如果此时没有 DNS 缓存的话,那么此时会先进行 DNS 解析后再创建连接。而如果使用了 DNS prefetch,DNS 解析的时机就可以提前,使得真正加载第三方站点资源的时候无需再进行 DNS 解析。
对于同站点的资源则无需进行 DNS prefetch,因为加载这些资源的时候本身就已经会存在 DNS 缓存了。

Preconnect

Preconnect 跟 DNS prefetch 类似,只是它做的事情比 DNS prefetch 更多,它不仅可以提前解析好 DNS,还能提前创建 TCP 连接,以及如果有必要则进行 TLS 协商。下图则是一个简单的示意图:
/image/b86f8f42-055c-4d1f-a082-b5c34cab30a5/79adfa86-8a13-477a-9db5-3a8c4f24d147_Untitled.png
对比上面 DNS prefetch 的图片,可以看到当使用了 preconnect 之后,字体文件的 DNS,TCP 以及 SSL 解析时机提前了,总体的页面加载时间也因此缩短了。

Preconnect 使用方法

与 DNS prefetch 类似,只需要把 rel 属性设置成 preconnect 即可:
<link rel="preconnect" href="//example.com">

Preconnect 适用场景

DNS prefetch 适用的场景,也都适用于 preconnect。理论上来说,应该优先使用 preconnect ,因为 preconnect 做的事情更多,能够提升的空间也就更大。但是 preconnect 诞生的时机比 DNS prefetch 晚很多,因此存在兼容性问题。所以作为最佳实践,可以两者一起声明。

Prerender

Prerender 主要是用来提前渲染某个页面。它不仅会加载这个页面,还会一并下载这个页面所依赖的所有资源,并且执行它们的逻辑。这看起来就相当于创建了一个隐藏的 Tab 去加载某个页面,当用户真正访问这个页面的时候,浏览器再把这个隐藏的 Tab 内容显示出来。

Prerender 使用方法

把 rel 属性设置成 prerender 即可:
<link rel="prerender" href="https://www.google.com" >

Prerender 适用场景

事实上,你应该小心地使用 prerender ,除非你确保用户肯定会访问这个 prerender 的页面,你才需要设置 prerender 。否则滥用 prerender 的话会造成浪费用户的带宽流量。
除此之外,由于 prerender 是会执行页面的 JS 逻辑,下载页面的所有资源依赖,这里面很有可能就包含了统计功能,因此使用 prerender 可能会造成统计功能不准确。所以使用 prerender 之前,你应该确保你的页面逻辑不会受这个特性所影响。

总结

Preload,prefetch,DNS prefetch 以及 preconnect 是目前较为常用的提升网页加载性能的手段。其中 preload 功能最为强大,如果合理利用的话能够减少页面白屏时间,加快页面加载。而 prerender 应用场景会比较少,因为其副作用大。开发者应该谨慎使用它。
这几种优化手段均是通过声明一条简单的 <link> 标签来实现的,可见 <link> 标签有多强大。如果你的页面现在还没有使用到上面提到的优化手段的话,你该检查检查了!让用户更快地打开你的网页,是每个开发人员必须掌握的技能。