关于预加载

对于前端来说,静态资源的加载对页面性能至关重要,如果能优化资源加载的顺序和时机并且不影响页面onload,将对性能提升帮助很大.


观察下图存在的问题:

图中,礼券页图片加载缓慢,出现渐显的效果.
问题的原因也很明显,优惠券列表展开时需要去加载图片,背景渐显的过程实际上就是图片加载的过程;当网速慢的时候,这个问题会更加明显.那么通过哪些形式能解决这个问题呢?

预加载的几种形式

image标签

1
<img src="">

实际上img标签即可实现预加载,将img标签放在礼券页之前并隐藏,保证其图片资源获取成功,再打开礼券页时,就会获取img标签带来的缓存.

css内联式

使用内联图片,也就是将图片转换为base64编码的data-url。这种方式,其实是将图片的信息集成到css文件中,避免了图片资源的单独加载。

js代码对图片进行预加载

1
2
3
4
5
6
7
8
9
10
preloadImage() {
const imgList = [
require('@/assets/imgs/error.png'),
require('@/assets/imgs/ticket_bg.png')
];
for (let i = 0; i < imgList.length; i++) {
const newIMG = new Image();
newIMG.src = imgList[i];
}
}

其实js方法的原来和img标签类似,关键在于Image对象并设置src.这样就起到了提前获取资源的作用.

上述方法都存在一定问题,无故增加了不必要的代码,冗杂,逻辑不清晰


浏览器提供的两个指令preload和prefetch就能一定程度上解决这个问题.

1
2
<link rel="prefetch" href="xxx">
<link rel="preload" href="xxx" as="xx">

prefetch

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

1
2
3
4
5
<head>
...
<link rel="prefetch" href="static/img/ticket_bg.a5bb7c33.png">
...
</head>

针对上例,用prefetch就非常适合,图片是再弹框后显示的,能够满足空闲下载和用户将来可能访问的特点.
在当前页面时,浏览器会预取图片资源,network中200但不返回实质资源,当礼券弹出,直接在prefetch cache(属于内存缓存)中获取图片资源.

preload

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

来看实例:

例中问题在于,页面字体加载慢,明显看出字体替换的闪烁.
针对这个问题,用preload,在当前页面提升字体加载优先级

1
2
3
4
5
6
<head>
...
<link rel="preload" as="font" href="./assets/fonts/AvenirNextLTPro-Demi.otf" crossorigin>
<link rel="preload" as="font" href="./assets/fonts/AvenirNextLTPro-Regular.otf" crossorigin>
...
</head>

在network中查看,font获取会在最开始获取.

preload link必须设置as属性来声明资源的类型(font/image/style/script,audio,video等),否则浏览器可能无法正确加载资源。
字体资源必须设置crossorigin,否则即使同源,浏览器会采用匿名模式的CORS去preload,导致两次请求无法共用缓存。

在vue中或用webpack打包的项目中,不可能每个资源通过link设置预加载(提取),因为构建产生hash后缀会变化.那么怎么在实践中使用呢?

Preload 和 Prefetch 的具体实践

preload-webpack-plugin

webpack插件preload-webpack-plugin可以帮助我们将该过程自动化,结合htmlWebpackPlugin在构建过程中插入link标签。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
const PreloadWebpackPlugin = require('preload-webpack-plugin');
...
plugins: [
new PreloadWebpackPlugin({
rel: 'preload'
as(entry) { //资源类型
if (/\.css$/.test(entry)) return 'style';
if (/\.woff$/.test(entry)) return 'font';
if (/\.png$/.test(entry)) return 'image';
return 'script';
},
include: 'asyncChunks', // preload模块范围,还可取值'initial'|'allChunks'|'allAssets',
fileBlacklist: [/\.svg/] // 资源黑名单
fileWhitelist: [/\.script/] // 资源白名单
})
]

详细配置

总结

preload的设计初衷是为了尽早加载首屏需要的关键资源,从而提升页面渲染性能。

目前浏览器基本上都具备预测解析能力,可以提前解析入口html中外链的资源,因此入口脚本文件、样式文件等不需要特意进行preload。

但是一些隐藏在CSS和JavaScript中的资源,如字体文件,本身是首屏关键资源,但当css文件解析之后才会被浏览器加载。这种场景适合使用preload进行声明,尽早进行资源加载,避免页面渲染延迟。

prefetch声明的是将来可能访问的资源,因此适合对异步加载的模块、可能跳转到的其他路由页面进行资源缓存;对于一些将来大概率会访问的资源.

  • 大部分场景下无需特意使用preload
  • 类似字体文件这种隐藏在脚本、样式中的首屏关键资源,建议使用preload
  • 异步加载的模块(典型的如单页系统中的非首页)建议使用prefetch
  • 大概率即将被访问到的资源可以使用prefetch提升性能和体验
  • preload和prefetch不会阻塞页面的onload。
  • 不要滥用preload和prefetch,需要在合适的场景中使用。
  • preload的字体资源必须设置crossorigin属性,否则会导致重复加载。
    原因是如果不指定crossorigin属性(即使同源),浏览器会采用匿名模式的CORS去preload,导致两次请求无法共用缓存。
  • 没有合法https证书的站点无法使用prefetch,预提取的资源不会被缓存
  • 两者的兼容性目前都还不是太好。好在不支持preload和prefetch的浏览器会自动忽略它,因此可以将它们作为一种渐进增强功能,优化我们页面的资源加载,提升性能和用户体验。

Q&A

当页面 preload 已经在 Service Worker 缓存及 HTTP 缓存中的资源时会发生什么?

如果资源没过期,那么 preload 会从相同的资源中获得缓存命中.

这种加载方式会浪费用户的带宽吗

使用 preload 或 prefetch,可能会浪费用户的带宽,特别是在资源没有缓存的情况下。因为有些资源可能不会用到,特别是prefetch是预测用户可能行为的.所以使用需要合理.

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

  1. preload 和 prefetch同时使用请求同一资源,(将prefetch作为preload的备用方案)

  2. preload 未提供有效的“as”,则最终将获取两次(预加载无效,实际用到资源时又被加载一次)。

  3. preload 字体不带 crossorigin

1
2
3
4
5
6
7
const preloadSupported = () => {
const link = document.createElement('link');
const relList = link.relList;
if (!relList || !relList.supports)
return false;
return relList.supports('preload');
};

可以使用 preload 让CSS样式立即生效吗?

1
<link rel="preload" href="style.css" onload="this.rel=stylesheet">

但没必要,因为与浏览器正常加载css差别不大

不是可以用 HTTP/2 的服务器推送(Server Push)来代替 preload 吗?

HTTP/2 PUSH(推送)与HTTP Preload(预加载)大比拼
当你知道资源的精确加载顺序时使用推送,使用 preload 可以使资源的开始下载时间更接近初始请求 - 这对所有的资源获取都有用。

我们假设浏览器正在加载一个页面,页面中有个 CSS 文件,CSS 文件又引用一个字体库,对于这样的场景,

若使用 HTTP/2 PUSH,当服务端获取到 HTML 文件后,知道后续客户端会需要字体文件,它就立即主动地推送这个文件给客户端,如下图:

客户端请求html页面,服务端知道页面中需要字体,主动推送字体

而对于 preload,服务端就不会主动地推送字体文件,在浏览器获取到页面之后发现 preload 字体才会去获取,如下图:

虽然推送很有效,但它不像 preload 那样对所有的情况都适应。

推送不能用于第三方资源的内容,通过立即发送资源,它还有效地缩短浏览器自身的资源优先级情况。在你明确的知道在做什么时,这应该会提高你的应用性能,如果不是很清晰的话,你也许会损失掉部分的性能。

peload 请求头是什么?它与 preload 标签相比如何?它与 HTTP/2 服务器推送有什么关系?


参考:

使用 Preload&Prefetch 优化前端页面的资源加载
Web 性能优化:Preload,Prefetch的使用及在 Chrome 中的优先级