在上篇文章 探究网页资源究竟是如何阻塞浏览器加载的 中介绍到 JS 会阻塞 DOM 的加载,样式会阻塞页面的渲染,外链样式里的自定义字体还会对文字造成闪动给用户带来不好的体验,诸如此类问题还有挺多,那到底该如何解决它们呢?

今天我们就来学习通过在 link 标签里加上特定的属性,比如 preloadprefetch 等来解决此类问题,那么你对这些属性又了解多少呢?把它们用在了你们的项目优化中了嘛?

preload

preload 提升了资源加载的优先级,使得它提前开始加载(预加载),在需要用的时候能够更快的使用上。另外 onload 事件必须等页面所有资源都加载完成才触发,而当给某个资源加上 preload 后,该资源将不会阻塞 onload

preload 怎么用

当某个页面加载了 2 个脚本 jquery.min.jsmain.js

1
2
<script src="https://cdn.bootcss.com/jquery/2.1.4/jquery.min.js"></script>
<script src="./main.js"></script>

此时该页面的资源加载 Waterfall 长这样:

当在 <head> 里通过 <link> 标签给 main.js 配置 preload 预加载后:

1
<link rel="preload" as="script" href="./main.js">

此时的 main.js 加载顺序出现在了 jquery.min.js 的前面,这就是 preload 提升资源加载优先级的效果。

preload main.js

当一直刷新浏览器的时候,偶然出现 Waterfall 并不能准确的显示资源加载的顺序,所以这个时候就需要比较每个资源被加入到下载队列的时间,比如如下的 main.js 由于用了 preload 预加载,所以 queue time 比较早。

preload main.js
preload main.js

通过 <link rel="preload"> 只是预加载了资源,但是资源加载完成后并不会执行,所以需要在想要执行的地方通过 <script> 来引入它:

1
<script src="./main.js"></script>

但是也有一个例外,因为 CSS 的加载也是通过 <link> 标签引入的,所以我们可以巧妙的利用这点,当 onload 事件触发的时候修改 rel 属性的值,使得它由原来的预加载样式变成引入样式:

1
<link rel="preload" as="style" onload="this.rel='stylesheet'" href="https://cdn.bootcss.com/bootstrap/4.0.0-alpha.6/css/bootstrap.css"/>

如果通过 preload 加载了资源,但是又没有使用它,则浏览器会报一个警告:

preload main.js

preload 除了能够预加载脚本之外,还可以通过 as 指定别的资源类型,比如:

  • style 样式表;
  • font:字体文件;
  • image:图片文件;
  • audio:音频文件;
  • video:视频文件;
  • document:文档。

preload 应用案例

preload 主要用于提升当前页面某些阻塞资源的下载优先级,使得页面能够尽快渲染显示出来。

案例一:预加载定义在 CSS 中资源的下载,比如自定义字体

当页面中使用了自定义字体的时候,就必须在 CSS 中引入该字体,而由于字体必须要等到浏览器下载完且解析该 CSS 文件的时候才开始下载,所以对应页面上该字体处可能会出现闪动的现象,为了避免这种现象的出现,就可以使用 preload 来提前加载字体,type 可以用来指定具体的字体类型,加载字体必须指定 crossorigin 属性,否则会导致字体被加载两次

1
<link rel="preload" as="font" crossorigin type="font/woff2" href="myfont.woff2">

以上这种写法和指定 crossorigin="anonymous" 是等同的效果。

案例二:预加载 CSS 文件

在首屏加载优化中一直存在一种技术,叫做抽取关键 CSS,意思就是把页面中在视口中出现的样式抽出一个独立的 CSS 文件出来 critical.css,然后剩余的样式在放到另外一个文件上 non-critical.css

由于 CSS 会阻塞页面的渲染,当同时去加载这 2 部分样式的时候,只要 non-critical.css 还没加载完成,那么页面就显示不了,而实际上只需要显示出视口下的界面即可,所以期待的结果是:当加载完成 critical.css 的时候马上显示出视口下的界面,不让 non-critical.css 阻塞渲染,则需要给 non-critical.css 加上预加载:

1
2
3
<link rel="preload" as="style" href="https://bubuzou.com/non-critical.css">
<link rel="stylesheet" href="https://bubuzou.com/critical.css">
<link rel="stylesheet" href="https://bubuzou.com/non-critical.css">

案例三:创建动态的预加载资源

当需要预先加载的时候调用 downloadScript,而希望执行的时候则调用 runScript 函数。

1
2
3
4
5
6
7
8
9
10
11
12
function downloadScript(src) {
var el = document.createElement("link")
el.as = "script"
el.rel = "preload"
el.href = src
document.body.appendChild(el)
}

function runScript(src) {
var el = document.createElement("script")
el.src = src
}

案例四:结合媒体查询预加载响应式图片

preload 甚至还可以结合媒体查询加载对应尺寸下的资源,对于以下代码当可视区域尺寸小于 600px 的时候会提前加载这张图片。

1
<link rel="preload" as="image" href="someimage.jpg" media="(max-width: 600px)">

案例五:结合 Webpack 预加载 JS 模块

Webpack4.6.0 版本开始支持在魔术注释中配置预加载模块:

1
import(_/* webpackPreload: true */_ "CriticalChunk")

如果是版本比较老的,则可以使用 preload-webpack-plugin 进行处理。

prefetch

preload 用于提前加载用于当前页面的资源,而 prefetch 则是用于加载未来(比如下一个页面)会用到的资源,并且告诉浏览器在空闲的时候去下载,它会将下载资源的优先级降到最低。

比如在首页配置如下代码:

1
<link rel="prefetch" as="script" href="https://cdn.bootcss.com/jquery/2.1.4/jquery.min.js">

我们会在页面中看到该脚本的下载优先级已经被降低为 Lowest

当资源被下载完成后,会被存到浏览器缓存中,当从首页跳转到页面 A 的时候,假如页面 A 中引入了该脚本,那么浏览器会直接从 prefetch cache 中读取该资源,从而实现资源加载优化。

preconnect

当浏览器向服务器请求一个资源的时候,需要建立连接,而建立一个安全的连接需要经历以下 3 个步骤:

  • 查询域名并将其解析成 IP 地址(DNS Lookup);
  • 建立和服务器的连接(Initial connection);
  • 加密连接以确保安全(SSL);

以上 3 个步骤浏览器都需要和服务器进行通信,而这一来一往的请求和响应势必会耗费不少时间。

而就基于这点上,可以使用 preconnect 或者 dns-prefetch 进行优化,而它两又是什么呢?怎么使用呢?

preconnect 是什么,怎么用

当我们的站点需要对别的域下的资源进行请求的时候,就需要和那个域建立连接,然后才能开始下载资源,如果我都已经知道了是和哪个域进行通信,那不就可以先建立连接,然后等需要进行资源请求的时候就可以直接进行下载了。

假设当前站点是 https://a.com,这个站点的主页需要请求 https://b.com/b.js 这个资源。对比正常请求和配置了 preconnect 时候的请求,它们在请求时间轴上看到的表现是不一样的:

通过如下配置可以提前建立和 https://b.com 这个域的连接:

1
<link rel="preconnect" href="https://b.com">

通过 preconnect 提早建立和第三方源的连接,可以将资源的加载时间缩短 100ms ~ 500ms,这个时间虽然看起来微不足道,但是它是实实在在的优化了页面的性能,提升了用户的体验。

通过 preconnect 和别的域建立连接后,应该尽快的使用它,因为浏览器会关闭所有在 10 秒内未使用的连接。不必要的预连接会延迟其他重要资源,因此要限制 preconnect 连接域的数量。

preconnect 应用场景

场景一:

当知道资源是来源于哪个源下,但是对于加载哪个资源不是很明确的时候,比如对于如下这些资源:


它们要嘛是动态的,要嘛是根据不同环境携带不同参数,所以它们很适合用 preconnect 进行加载。

场景二:

如果页面上有流媒体,但是没那么快播放,又希望当按下播放按钮的时候可以越快开始越好,此时就可以使用 preconnect 预建立连接,节省一段时间。

如果用 preconnect 预建立连接的资源是一个字体文件,那么也是需要加上 crossorigin 属性。

dns-prefetch

通常我们记住一个网站都是通过它的域名,但是对于服务器来说,它是通过 IP 来记住它们的。浏览器使用 DNS 来将站点转成 IP 地址,这个是建立连接的第一步,而这一步骤通常需要花费的时间大概是 20ms ~ 120ms。因此,可以通过 dns-prefetch 来节省这一步骤的时间。

居然能通过 preconnect 来减少整个建立连接的时间,那为什么还需要 dns-prefetch 来减少建立连接中第一步 DNS 查找解析的时间呢?

假如页面引入了许多第三方域下的资源,而如果它们都通过 preconnect 来预建立连接,其实这样的优化效果反而不好,甚至可能变差,所以这个时候就有另外一个方案,那就是对于最关键的连接使用 preconnect,而其他的则可以用 dns-prefetch

可以按照如下方式配置 dns-prefetch

1
<link rel="dns-prefetch" href="https://cdn.bootcss.com">

另外由于 preconnect 的浏览器兼容稍微比 dns-prefetch 低,看下图:

preconnect
dns-prefetch

因此 dns-prefetch 可以作为不支持预连接的浏览器的后备选择,同时配置它们两即可:

1
2
<link rel="preconnect" href="https://cdn.bootcss.com">
<link rel="dns-prefetch" href="https://cdn.bootcss.com">

参考文章