性能优化之prefetch、dns-prefetch、preload、preconnect、prerender

在做项目优化的时候,遇到了 prefetchpreload,之前没有总结过,所以今天顺带都总结下。

首先这些属性都是在 link 标签上使用,以前都是拿 link 标签来加载外部css,没想到还有这个妙用。

preload

示例代码

<link href="app.9dad33ea7f9b9e219e85.js" rel="preload" as="script">
<link href="css/app.613f0ba0.css" rel="preload" as="style">

作用 (提前加载)

preload 是一个高优先级的资源加载声明,专注于当前页面。

通过 Chrome devtools 的 network 面板,可以看到带有 <link rel="preload"> 的资源会优先发送加载的请求。

那么到底会优先到啥程度呢? MDN 是这么说的

对于这种即刻需要的资源,你可能希望在页面加载的生命周期的早期阶段就开始获取,在浏览器的主渲染机制介入前就进行预加载。这一机制使得资源可以更早的得到加载并可用,且更不易阻塞页面的初步渲染,进而提升性能。

主渲染机制介入前?有点抽象啊,没关系,我们用 chrome 的Performance 来形象的看一下,如图

image

在下载了文本文档之后,在 parse HTML 之前就开始请求带有 rel="preload" ,好家伙,这个优先级确实够高的,不过要注意,这里只是提前加载,加载完后会将他们存储在浏览器的缓存中,并不会马上执行(js),等 parse HTML 的时候,解析到对应的资源标签,再从缓存中获取。

如果说不用 preload 那文件啥时候加载呢,理论上是在 parse HTML 之后,因 html 是逐行解析的,所以,当解析到 js、 css 对应的 script 和 link 标签时才会去请求加载。为了验证下这个想法,把 html 里面的 preload 都去掉之后,再看下(用anywhere本地起个服务,上张图是用的线上测试环境的,所有有些加载时间会不太一样,这里只是验证想法)

image

通过两张图的对比,应该能很清晰的看出来区别了吧。

支持哪些类型的内容预加载

  • audio: 音频文件。
  • document: 一个将要被嵌入到<frame><iframe>内部的HTML文档。
  • embed: 一个将要被嵌入到<embed>元素内部的资源。
  • fetch: 那些将要通过fetch和XHR请求来获取的资源,比如一个ArrayBuffer或JSON文件。
  • font: 字体文件。
  • image: 图片文件。
  • object: 一个将会被嵌入到<embed>元素内的文件。
  • script: JavaScript文件。
  • style: 样式表。
  • track: WebVTT文件。
  • worker: 一个JavaScript的web worker或shared worker。
  • video: 视频文件。

其他优点:

  • 更精确地优化资源加载优先级。
  • 匹配未来的加载需求,在适当的情况下,重复利用同一资源。
  • 为资源应用正确的内容安全策略
  • 为资源设置正确的 Accept 请求头。

兼容性:

再通过caniuse看下兼容性,覆盖率92%,可以放心用。
image

prefetch

示例代码

<link href="css/chunk-857c83aa.64e71df7.css" rel="prefetch">
<link href="js/chunk-fe156828.e388cae8.js" rel="prefetch">

作用 (预判加载)

prefetch 是低优先级的资源加载声明,主要是利用浏览器空闲时间来下载后续页面或者之后要用到的资源。

链接预取是一种浏览器机制,其利用浏览器空闲时间来下载或预取用户在不久的将来可能访问的文档。网页向浏览器提供一组预取提示,并在浏览器完成当前页面的加载后开始静默地拉取指定的文档并将其存储在缓存中。当用户访问其中一个预取文档时,便可以快速的从浏览器缓存中得到。

浏览器的空闲时间如何确定的?

MDN 说:
在目前(Moilla 1.2),空闲时间的确定是通过 nsIWebProgressListener API 实现的。我们在顶层 nsIWebProgress 对象 ("@mozilla.org/docloaderservice;1") 上附加了一个监听器。通过监听器,我们能够收到文档开始和停止的通知,从而将最后一个文档停止到下一个文档开始之间的间隔作为空闲时间的近似值。最后一个文档停止的通知大致会在顶层文档的 onLoad 方法即将被触发时发出。此时即是开始预取的时间点。如果一个子文档包含了预取提示,这些预取操作将会等到最顶层文档和其子文档完成加载后才会开始进行。

这个说法也太不好理解了,还是直接看实际的例子吧
这个是页面代码:

<!DOCTYPE html>
<html>
    <head>
        <meta charset=utf-8>
        <meta name=viewport content="width=device-width,initial-scale=1,maximum-scale=1,minimum-scale=1,user-scalable=no,minimal-ui">
        <meta name=apple-mobile-web-app-capable content=yes>
        <meta name=apple-mobile-web-app-status-bar-style content=black>
        <meta name=format-detection content="telephone=no">
        <title></title>
        <script>
            if (navigator.userAgent.indexOf('wxwork') >= 0) {
                document.write('<script src="https://res.wx.qq.com/open/js/jweixin-1.2.0.js"><' + '/script>');
            }
        </script>
        <link href=css/chunk-0055c1c8.66e20caf.css rel=prefetch>
        <link href=css/chunk-00bb76a1.5bf996b0.css rel=prefetch>
        <link href=css/chunk-0876ddcd.8723c688.css rel=prefetch>
                               省略部分prefetch...
        <link href=js/chunk-ff60911c.55f5dc7f.js rel=prefetch>
        <link href=css/chunk-vendors.a918e666.css rel=stylesheet>
        <link href=css/app.613f0ba0.css rel=stylesheet>
    </head>
    <body id=body>
        <div id=app></div>
        <script src=js/chunk-vendors.7b8d79e4.js></script>
        <script src=app.2c32b69b5680a5e5a291.js></script>
    </body>
</html>

来看下加载的顺序:
image

这么看貌似没什么问题,符合预期,确实是在这几个请求之后开始的。

<link href=css/chunk-vendors.a918e666.css rel=stylesheet>
<link href=css/app.613f0ba0.css rel=stylesheet>
<script src=js/chunk-vendors.7b8d79e4.js></script>
<script src=app.2c32b69b5680a5e5a291.js></script>

但是在 vue 的项目中,会动态生成几个这种标签,比如首页组件对应的css、js。如下图:
image

再看下请求顺序:
image

因为这几个标签是 app.js 动态添加的,在这之前,浏览器已经判定为空闲时间,已经开始了下载任务,结果又来了几个重要的任务,prefetch 的优先级虽然是 lowest,但是开始时间比普通的 link[css] 、script[js] 却更早,prefetch下载会占用带宽,是会阻塞后面资源下载(从上图看应该是没有暂停prefetch,依然是竞争关系)。link[css]的优先级是highest,script[js]的优先级是low。

所以为了提高首屏速度,还是把 prefetch 去掉或者 将 prefetch 用js动态延迟加载。

兼容性:

image

dns-prefetch

作用 (dns 预解析)

dns-prefetch 主要是为后面要加载的文件或打开的链接提供提前解析域名的功能。

当浏览器从(第三方)服务器请求资源时,必须先将该跨域域名解析为 IP地址,然后浏览器才能发出请求。此过程称为 DNS解析。DNS 缓存可以帮助减少此延迟,而 DNS解析可以导致请求增加明显的延迟。对于打开了与许多第三方的连接的网站,此延迟可能会大大降低加载性能。

dns解析时间对比

dns 解析时间其实挺影响首屏页面加载速度的,不过在说这个问题之前还是要搞懂dns解析流程

其实不同网站dns 解析时间是不太一样的,比如淘宝、京东、知乎之类的大网站,在本地dns 服务器或者dns根、dns顶级域名服务器一般是有缓存的,所以他们的查询流程会比小网站短,所以查询速度会快一些,那dns解析速度一般会是多少呢,我们找几个网站对比一下看看
京东:
image

https://davidwalsh.name/html5-prefetch(之前没有访问过的网站):
image

经常访问的大网站的dns解析时间一般在20-30ms 左右,而没怎么访问过的小网站需要200-300ms左右,相差10倍左右,如果单是dns解析就要300ms的话,首屏做到1s内难度就更高一些了,所以首屏的资源最好放在同域名下,这样会省去后续资源的dns解析时间。

应用场景

  • 比如导航网站 hao123 这种要跳转很多第三方的网站
    image
  • 淘宝这种大站,集合了很多其它域名的页面
    image

兼容性:

image

prerender

作用

prerender 和 prefetch 相似,它们都会在空闲时间加载之后可能会用到的资源,区别是 prerender 在后台渲染了整个页面所有的资源。如下:

Steve Souders 的文章详细解释了这个技术:

prerender 就像是在后台打开了一个隐藏的 tab,会下载所有的资源、创建 DOM、渲染页面、执行 JS 等等。如果用户进入指定的链接,隐藏的这个页面就会进入马上进入用户的视线。Google Search 多年前就利用了这个特性实现了 Instant Pages 功能。微软最近也宣布会让 Bing 在 IE11 上用类似 prerender 的技术。

<link rel="prerender" href="https://www.google.com">

我自己尝试了下,并没有看到有任何的资源下载,不清楚是什么问题。暂时就当学习了

兼容性:

image

preconnect

作用

和 DNS prefetch 类似,preconnect 不光会解析 DNS,还会建立 TCP 握手连接和 TLS 协议(如果需要)。用法如下:

<link rel="preconnect" href="http://css-tricks.com">

Ilya Grigorik 写了一篇文章详细说明了这种技术:

现代浏览器竭尽所能的尝试预测网站可能需要哪些链接。通过提前连接,浏览器可以提前建立必要的通信,消除了实际请求中 DNS、TCP 和 TLS 的耗时。不过,即使是只能的现代浏览器,也没办法为每个网站可靠的预测所有连接。

幸运的是开发者可以告诉浏览器哪些通信需要在实际请求发起前就提前建立连接。

大佬的解释比 MDN 好太多了,膜拜!!!

这个真的是牛啊,直接把资源下载前的工作全干了,只需要等待资源下载就行了。
来看下使用前后网络请求对比图:
image

但是声明了 preconnect 不代表浏览器一定会去做,浏览器也可以选择忽略的(可能没有好的时机来处理,这完全可以取决于浏览器具体的实现)。

兼容性:

image

参考:
https://developer.mozilla.org/zh-CN/docs/orphaned/Web/HTML/Preloading_content
http://www.alloyteam.com/2015/10/prefetching-preloading-prebrowsing/
https://zhuanlan.zhihu.com/p/33179052