使用 Preload&Prefetch 优化前端页面的资源加载

对于前端页面来说,静态资源的加载对页面性能起着至关重要的作用。本文将介绍浏览器提供的两个资源指令-preload/prefetch,它们能够辅助浏览器优化资源加载的顺序和时机,提升页面性能。

一、从一个实例开始

如上图所示,我们开发了一个简单的收银台,支付过程中可以展开优惠券列表选择相应的券。从动图可以看到,列表第一次展开时,优惠券背景有一个逐渐显示的过程,体验上不是很好。

问题的原因也很明显,由于背景使用了视觉特意设计的图片,优惠券列表展开时需要去加载图片,背景渐显的过程实际上就是图片加载的过程;当网速慢的时候,这个问题会更加明显。那么,怎样解决这个问题呢?

仔细分析一下,我们会发现问题的原因在于背景图的加载时机太晚。

如果能在优惠券列表渲染前加载好背景图,这个问题就不会出现。从这个思路出发,我们可能想到以下两个方案:

  1. 使用内联图片,也就是将图片转换为base64编码的data-url。这种方式,其实是将图片的信息集成到css文件中,避免了图片资源的单独加载。但图片内联会增加css文件的大小,增加首屏渲染的时间。
  2. 使用js代码对图片进行预加载
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代码在特定时机提前加载相应图片,优惠券列表渲染时就可以直接从缓存获取。不过,这种方案增加了额外的代码,需要自己控制好加载时机,并且将图片的url硬编码在了逻辑中。

可以看出,以上两种方案能够解决我们的问题,但都存在一些缺点。

那么,有没有更好的解决方案呢?答案是prefetch-一种由浏览器原生提供的预加载方案。

二、什么是prefetch?

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

具体来说,浏览器通过<link rel="prefetch" href="/library.js">标签来实现预加载。

其中rel="prefetch"被称为Resource-Hints(资源提示),也就是辅助浏览器进行资源优化的指令。

类似的指令还有rel="preload",我们会在后文提及。

从prefetch的定义可以知道,设置了prefetch的资源会在浏览器空闲时进行加载,这恰恰可以解决我们的问题。在入口html文件head加入prefetch标签。

<head>
    <link rel="prefetch" href="static/img/ticket_bg.a5bb7c33.png">
</head>

查看现在优惠券列表的加载效果。

果然,成功达成了我们期望的效果。那么浏览器是如何做的呢?我们打开Chrome的Network面板一探究竟:

可以看到,在首屏的请求列表中已经出现了优惠券背景图ticket_bg.png的加载请求,请求本身看起来和普通请求没什么不同;展开优惠券列表后,network中增加了一次新的ticket_bg.png访问请求,我们很快发现,这个请求的status虽然也是200,但有一个特殊的标记—prefetch cache,表明这次请求的资源来自prefetch缓存。这个表现验证了上文中prefetch的定义,即浏览器在空闲时间预先加载资源,真正使用时直接从浏览器缓存中快速获取。

三、Preload

从上面的案例,我们体会到了浏览器预加载资源的强大能力。实际上,预加载是一个广义的概念,prefetch只是具体实现方式之一,本节我们介绍下另外一种预加载方式preload。上文我们提到,preload与prefetch同属于浏览器的Resource-Hints,用于辅助浏览器进行资源优化。为了对两者进行区分,prefetch通常翻译为预提取,preload则翻译为预加载。

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

简单来说,就是通过<link rel="preload" href="xxx" as="xx">标签显式声明一个高优先级资源,强制浏览器提前请求资源,同时不阻塞文档正常onload。我们同样用一个实际案例进行详细介绍。

上图是我们开发的另外一个收银台,出于本地化的考虑,设计上使用了自定义字体。开发完成后我们发现,页面首次加载时文字会出现短暂的字体样式闪动(FOUT,Flash of Unstyled Text),在网络情况较差时比较明显(如动图所示)。究其原因,是字体文件由css引入,在css解析后才会进行加载,加载完成之前浏览器只能使用降级字体。也就是说,字体文件加载的时机太迟,需要告诉浏览器提前进行加载,这恰恰是preload的用武之地。

我们在入口html文件head加入preload标签:

<head>
    <link rel="preload" as="font" href="<%= require('/assets/fonts/AvenirNextLTPro-Demi.otf') %>" crossorigin>
    <link rel="preload" as="font" href="<%= require('/assets/fonts/AvenirNextLTPro-Regular.otf') %>" crossorigin>
</head>

再次查看页面首次加载的效果:


字体样式闪动的现象没有了!我们对比下使用preload前后的network面板。

使用前:

使用后:

可以发现字体文件的加载时机明显提前了,在浏览器接收到html后很快就进行了加载。

注意:preload link必须设置as属性来声明资源的类型(font/image/style/script等),否则浏览器可能无法正确加载资源。

四、Preload 和 Prefetch 的具体实践

1、preload-webpack-plugin

前文中我们举的两个例子,都是在入口html手动添加相关代码:

<head>
    <link rel="prefetch" href="static/img/ticket_bg.a5bb7c33.png">
</head>


<head>
    <link rel="preload" as="font" href="<%= require('/assets/fonts/AvenirNextLTPro-Demi.otf') %>" crossorigin>
    <link rel="preload" as="font" href="<%= require('/assets/fonts/AvenirNextLTPro-Regular.otf') %>" crossorigin>
</head>

这显然不够方便,而且将资源路径硬编码在了页面中(实际上,ticket_bg.a5bb7c33.png后缀中的hash是构建过程自动生成的,所以硬编码的方式很多场景下本身就行不通)。webpack插件preload-webpack-plugin可以帮助我们将该过程自动化,结合htmlWebpackPlugin在构建过程中插入link标签。

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';