之前介绍了利用Preload优化首屏关键资源的加载。今天跟大家介绍另一个性能优化手段——Prefetch。文末会结合常见工具,教大家在项目中使用Preload和Prefetch。
跟Preload类似,Prefetch也是Link的一种关系类型值,用于提示浏览器提前加载资源。跟Preload不同,Prefetch指示的是下一次导航可能需要的资源。浏览器识别到Prefetch时,应该加载该资源(且不执行),等到真正请求相同资源时,就能够得到更快的响应。
它的使用方式与Preload类似:
在HTML的
中:通过JS动态插入:
var hint = document.createElement("link");
hint.rel = "prefetch";
hint.as = "html";
hint.href = "/article/part3.html";
document.head.appendChild(hint);
在HTTP头中:Link: https://example.com/banner.jpg; rel=prefetch; as=image;
Prefetch的兼容性如下:
跟Preload比起来,Prefetch的兼容范围更广。唯独在Safari上对Preload的支持度比Prefetch要好。
由于Preload和Prefetch两个小朋友的名字太像了,行为也十分相似。它们站在一起的时候,很多人傻傻分不清楚。下面来说一说它俩的区别:
Prefetch vs Preload
1. 网络请求的优先级
在Chrome中,Prefetch的优先级为Lowest。而Preload的优先级则是根据as属性值所对应的资源类型来决定,总体上,Preload的优先级比Prefetch高。不过两者都不应该延迟页面的load事件。
2. 缓存策略
Preload加载的资源至少会被缓存到内存中,下一次请求的时候直接从缓存读取,从而减少从服务器加载的时间。
Prefetch的缓存并未在标准中定义,所以浏览器不保证缓存资源。不过会根据资源本身的缓存头进行相应的处理。
2017年Addy Osmani的“Preload, Prefetch and Priorities in Chrome”文章提到:
Furthermore, prefetch requests are maintained in the unspecified net-stack cache for at least 5 minutes regardless of the cachability of the resource.
意思是不论资源的缓存配置如何,Prefetch的请求会被维护在网络栈中至少5分钟。那么现在的Chrome中是不是这样呢?
笔者在Chrome 69中测试发现,如果资源配置了no-store,或者在开发者工具的网络面板中禁用缓存,浏览器并不会缓存该资源。下次请求还会再次从服务器加载资源。
(图中第一次index.js的请求是使用的Prefetch,第二次是正常请求。笔者在服务端做了延迟1s响应的处理,可以从加载时长看出第二次请求仍然是从服务器获取)
在不禁用缓存且配置了适当的缓存控制的情况下,下次请求则会从缓存加载(from disk cache),可以节约网络加载时间:
所以对于想要Prefetch的资源要做好缓存控制,以便下次请求时命中缓存。而对于动态HTML文档,则没必要使用Prefetch加载。
3. 重复加载
如果Preload的资源还在途中,此时对相同的资源再发起请求,浏览器不会重复请求资源,而是等返回了再进行处理。
而如果Prefetch的资源还在途中,再发请求,会导致二次请求(如上面“缓存策略”所示)。除此之外,有人可能会将Prefetch作为Preload的降级方案紧跟在Preload后面,也会产生两次请求,如下图所示:
4. 页面跳转时的行为
如果在当前页面跳转到下一页,在途的Preload请求会被取消。
而Prefetch的请求会在导航过程中保持,如下图所示:
document.getElementById('btn').addEventListener('click', () => {
var hint = document.createElement("link");
hint.rel = "prefetch";
hint.as = "img";
hint.href = "https://p1.ssl.qhimg.com/t01709ea6aebf12d69f.jpg";
document.head.appendChild(hint);
location.href="https://code.h5jun.com/cuma"
})
(第一个图片请求在跳转时没有被取消)
5. 适用场景
Preload的设计初衷是为了让当前页面的关键资源尽早被发现和加载,从而提升首屏渲染性能。
Prefetch是为了提前加载下一个导航所需的资源,提升下一次导航的首屏渲染性能。但也可以用来在当前页面提前加载运行过程中所需的资源,加速响应。
那么在生产环境中如何方便地使用Preload和Prefetch呢?
实践:单页应用中的Preload和Prefetch
关键资源:在单页应用中,应尽早加载关键资源。以React项目为例,应尽早加载React.js以及入口文件。如果项目使用Webpack和htmlWebpackPlugin,入口脚本文件和CSS都是直接输出到HTML中,大部分浏览器能预测解析这些资源,所以不必特意Preload这些资源。
但是有一些隐藏资源,比如font文件,则需要Preload。这种资源可以使用preloadWebpackPlugin,结合htmlWebpackPlugin,在编译阶段插入link rel="preload"标签。配置如下:
const preloadWebpackPlugin = require('preload-webpack-plugin')
...
// webpack配置
plugins: [
new htmlWebpackPlugin(),
new preloadWebpackPlugin({
as(entry) {
if (/\.woff2$/.test(entry)) return 'font';
return 'script';
},
include: 'allAssets',
rel: 'preload',
fileWhitelist: [/\.woff2/]
})
]
在HTML文件的头部会生成如下标签:
异步路由组件则应当在初始化后再加载。之前有读者朋友说异步路由组件应该用Prefetch,这个策略很好,只有一个地方需要注意:如果当前路由文件也是异步的,那么在Prefetch它的途中大概率会再次请求当前路由组件,从而导致二次加载。所以需要更细粒度的加载策略。
比如可以在鼠标移入导航菜单时再预加载其他的路由组件。既可以使用Prefetch也可以使用Preload。这里笔者认为使用Preload更优,因为Prefetch的优先级比较低而且容易引起二次加载。在React项目中,可以使用react-loadable管理异步组件的加载,它还提供了Loading状态和 preload方法:
const AboutComponent = Loadable({
loader: () => import(/* webpackChunkName: "about" */'./routes/about.js'),
loading: Loading
});
...
onMouseOver = (page) => {
AboutComponent.preload(); // 鼠标移入时再预加载相应路由组件
};
...
<li onMouseOver={() => {this.onMouseOver()}}><Link to="/about">关于</Link></li>
其他异步模块可以用Webpack的魔法注释:import(/* webpackPrefetch: true */ "...")、 import(/* webpackPreload: true */ "...") 。有一个细节需要注意:Preload魔法注释只有写在非入口文件的chunk中才能“动态”插入 link rel="preload"标签。感兴趣的小伙伴可以试试。