阅读 1010
前端预加载

前端预加载

提升前端页面的性能有很多方法,其中一类很重要的方法就是前端预加载,即提前加载后续需要的内容 (preload and prefetch)、提前解析域名(DNS Pretech & Preconnect)、预先渲染要加载的页面(prerender),这样在后续实际需要加载对应的内容时,有更快的加载速度。举个例子,在抖音 Web 端、阿里云控制台页面等,都能看到很多 rel 属性为 dns-prefetchlink 标签。

image.png

image.png

DNS Prefetch 与 Preconnect

DNS Prefetch

对于浏览器未链接过的的第三方域名,将域名解析为 IP 地址通常需要 20-120 毫秒,当 DNS 解析完成后浏览器才能发起请求。如果能提前执行域的 DNS 解析,那么访问该域名时能有更快的连接速度。这能提升网站性能,尤其对于那些需要从很多第三方域加载 CDN 资源的网站。

<link rel=“prefetch”>是尝试在请求资源之前解析域名。这可能是后面要加载的文件,也可能是用户尝试打开的链接目标。具体使用时,将 <link> 标签的 rel 属性指定为 dns-prefetch,并且在 href 属性中指定要提前进行 DNS 解析的域名。例如:

<link rel="dns-prefetch" href="https://fonts.googleapis.com/"> 
复制代码

Preconnect

<link rel=“dns-prefetch”>不同,初 DNS 解析之外,<link rel=“preconnect”>还会与服务器建立 TCP 链接以及进行 TSL 握手(对 HTTPS 站点而言),这能进一步减少建立跨域请求的时间,降低延迟。现代浏览器会尽最大努力在实际请求发出之前预测站点将需要什么连接:通过启动早期“预连接”,浏览器可以提前设置必要的套接字,并从实际请求的关键路径中消除昂贵的DNS、TCP和TLS往返。也就是说,尽管现代浏览器非常智能,但它们无法可靠地预测每个网站的所有预连接目标。通过 <link rel=“preconnect”>,跨域场景下在启动实际请求之前,可以告诉浏览器具体需要提前与哪些第三方网站建立连接。

<link rel="preconnect" href="https://fonts.gstatic.com/" crossorigin>
复制代码

虽然 preconnect 做的事比 dns-prefetch 更多,但旧版本浏览器对 preconnect 的支持程度不如 dns-prefetch。如下图所示,IE10 以及 Safari5 就已经支持 dns-prefetch ,但是 Safari 从 11.1 版本起起才支持 preconnect,并且 IE 浏览器不支持 preconnect。此外,由于与第三方域保持连接是一项比较昂贵的操作,因此 dns-prefetchpreconnect 更为轻量级。不建议对超过 4-6 个以上的第三方域使用 preconnect,当要加速的第三方域较多时,请使用 dns-prefetch

image.png

image.png

使用 DNS Prefetch 与 Preconnct 的注意事项

  • dns-prefetch 仅对跨域的 DNS 解析有效,因此请避免使用它来指向您的站点或域。这是因为,到浏览器看到提示时,您站点域背后的IP已经被解析。

  • 考虑将 dns-prefetch 与 preconnect一期使用。尽管 dns-prefetch 仅执行 DNS查找,但preconnect 会建立与服务器的连接。如果站点是通过HTTPS服务的,则此过程包括DNS解析,建立TCP连接以及执行TLS握手。将两者结合起来可提供进一步减少跨域请求的感知延迟的机会。如果页面需要建立与许多第三方域的连接,则将它们预先连接会适得其反,因此preconnect 提示最好仅用于最关键的连接。对于其他的,只需使用 <link rel="dns-prefetch"> 即可节省第一步的时间 - DNS 查找。

<link rel="preconnect" href="https://fonts.gstatic.com/" crossorigin>
<link rel="dns-prefetch" href="https://fonts.gstatic.com/">
复制代码
  • 不用对超链接做手动 dns prefetching,因为 chrome 会自动做 dns prefetching。 Chrome 会自动把当前页面的所有带href的link的dns都prefetch一遍。只有预计用户在后面的访问中需要用到当前页面的所有链接都不包含的域名,才需要手动需要手动添加 <link rel=“dns-prefetch”>。Chrome一直在做类似的事情。如果您在URL栏中键入一小部分域,它将自动预解析DNS(有时甚至预呈现页面),从而减少每个请求的关键毫秒数。

  • 在 Chrome 浏览器中,其他需要手动 dns prefetch 的场景:对静态资源域名做手动 dns prefetch ;对 js 里会发起的跳转做手动 dns prefetch;对重定向跳转的新域名做手动 dns prefetch

Preload & Prefetch

Preload

preload 提供了一种声明式的命令,能让浏览器提前加载指定资源(如脚本或者样式表),并在需要执行的时候再执行。这在希望加快某个资源的加载速度时很有用。在 preload 下载完资源后,资源只是被缓存起来,浏览器不会对其执行任何操作。不执行脚本,不应用样式表。使用方式如下:

<link rel="preload" href="/path/to/style.css" as="style" />
复制代码

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

  • 将加载和执行分离开,可不阻塞渲染和 document 的 onload 事件
  • 提前加载指定资源,不再出现依赖的 font 字体隔了一段时间才刷出
  • Preload 使开发能够自定义资源的加载逻辑,且无需忍受基于脚本的资源加载器带来的性能损失。可以使用 Preload 进行 CSS 资源的预加载、并且同时具备:高优先级、不阻塞渲染等特性。
  • 在首屏渲染(关键CSS)中用到一些隐藏资源,比如字体,较大的图片资源等。我们可以内联关键CSS的前面使用 标签提前预加载这些重要资源:
  <link rel="preload"  href="fonts/cicle_fina-webfont.woff2" as="font" type="font/woff2" crossorigin>
  <link rel=preload href=cat.png as=image imagesrcset="cat.png 1x, cat-2x.png 2x">
  <link rel="preload" as="image" href="important.png">
复制代码

注意:设置了 rel 属性的 link 标签 必须设置 as属性来声明资源的类型,否则浏览器可能无法正确加载资源。常见的 as 属性包括:

  • script: JavaScript 文件
  • style: CSS 样式文件
  • font: 字体文件
  • image: 图片文件
  • fetch: 通过 fetch 或者 XHR 获取的文件,例如 ArrayBuffer 或者 JSON 文件。

使用场景

字体提前加载

字体是较晚才能被发现的关键资源中常见的一种。但是在用户体验对前端来说至关重要的现阶段前端开发来说,web 字体对页面的渲染也是至关重要。字体的引用被深埋在 css 中,即便预加载器有提前解析 css,也无法确定包含字体信息的选择器是否会真正作用在 dom 节点上。所以为了减少 FOUT(无样式字体闪烁,flash of unstyled text )需要预加载字体文件。 有了 Preload,一行代码搞定。

<link rel="preload" href="font.woff2" as="font" type="font/woff2" crossorigin />
复制代码

动态加载脚本,但不执行

通常我们可能想在当前页去加载下一页的资源,但是在 preload 的情况下,我们常常使用动态创建 script 标签的形式,但是动态创建 script 标签的话,js 代码会立即执行。在有了preload之后,就可以做到动态加载,延迟执行

let link = document.createElement("link");
link.href = "myscript.js";
link.rel = "preload";
link.as = "script";
document.head.appendChild(link);
复制代码

上面这段代码可以让你预先加载脚本,下面这段代码可以让脚本执行

let script = document.createElement("script");
script.src = "myscript.js";
document.body.appendChild(script);
复制代码

基于标记语言的异步加载

<link rel="preload" as="style" href="asyncstyle.css" onload="this.rel='stylesheet'" />
复制代码

preload 的 onload 事件可以在资源加载完成后修改 rel 属性,从而实现非常酷的异步资源加载。

脚本也可以采用这种方法实现异步加载

<script async>, <scirpt defer> 可以异步加载 JS 文件,但是会阻塞 window 的 onload 事件。在某些情况下,你可能不希望阻塞 window 的 onload 。举个例子,你想尽可能快的加载一段统计页面访问量的代码,但又不愿意这段代码的加载给页面渲染造成延迟从而影响用户体验。这时候就可以利用 preload 来实现不阻塞 onload 的异步加载。

<link
  rel="preload"
  as="script"
  href="async_script.js"
  onload="let script = document.createElement('script'); script.src = this.href; document.body.appendChild(script);"/>
复制代码

Prefetch

prefetch (链接预取)是一种浏览器机制,其利用浏览器空闲时间来下载或预取用户在不久的将来可能访问的文档。网页向浏览器提供一组预取提示,并在浏览器完成当前页面的加载后开始静默地拉取指定的文档并将其存储在缓存中。当用户访问其中一个预取文档时,便可以快速的从浏览器缓存中得到。prefetch 是一个低优先级的资源提示,允许浏览器在后台空闲时获将来可能用得到的资源,并且将他们存储在浏览器的缓存中。一旦一个页面加载完毕就会开始下载其他的资源,然后当用户点击了一个带有 prefetched 的连接,它将可以立刻从缓存中加载内容。

<head>
    ...
    <link rel="prefetch" href="static/img/delay_load_img.a5bb7c33.png">
    ...
</head>
复制代码

假设 delay_load_img 是非首屏需要的图片的资源,设置 rel 属性为 prefetch 后,在首屏的请求列表中将会有对 delay_load_img 的加载请求。在后续使用 delay_load_img 的时候,network 中也会有对 delay_load_img 的访问请求。虽然这个请求的 status 也是 200,但有一个特殊的标记 prefetch cache,表明这次请求的资源来自prefetch缓存。

这个表现验证了上文中prefetch的定义,即浏览器在空闲时间预先加载资源,真正使用时直接从浏览器缓存中快速获取。与 dns prefetch 不同,prefetch 实际上是在请求和下载该资产,并将其存储在缓存中。但是,这取决于许多条件,因为浏览器可以忽略预取。 例如,客户端可能会在慢速网络上放弃对大字体文件的请求。

Preload & Prefetch 的区别

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

目前浏览器基本上都具备预测解析能力,可以提前解析入口 html 中外链的资源,因此入口脚本文件、样式文件等不需要特意进行preload。但是一些隐藏在CSS和JavaScript中的资源,如字体文件,本身是首屏关键资源,但当css文件解析之后才会被浏览器加载。 这种场景适合使用 preload 进行声明,尽早进行资源加载,避免页面渲染延迟。

preload 不同,prefetch 声明的是将来可能访问的资源,因此适合对异步加载的模块、可能跳转到的其他路由页面进行资源缓存;对于一些将来大概率会访问的资源,例如常见的加载失败icon,也较为适用。此外,此外,对于 SPA 应用,如果根据路由将代码进行了分割,不同路由对应的页面加载不同的 bundle,那么用户在访问某个页面时,可以利用 prefetch 预先加载改页面可能跳转的其他页面的 bundle。

preloadprefetch 不会阻塞页面的 onloadpreload 用来声明当前页面的关键资源,强制浏览器尽快加载 ;而 prefetch 用来声明将来可能用到的资源,在浏览器空闲时进行加载。总结如下:

  • 大部分场景下无需特意使用 preload。可以通过 Chrome 的 Devtool Performance 工具,来查看阻塞页面首屏渲染的的 JS 脚本或者 CSS 样式文件,并将其设置为 preload
  • 类似字体文件这种隐藏在脚本、样式中的首屏关键资源,建议使用 preload
  • 异步加载的模块(典型的如单页系统中的非首页)建议使用 prefetch
  • 大概率即将被访问到的资源可以使用 prefetch 提升性能和体验。

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';
    },
    include: 'asyncChunks', // preload模块范围,还可取值'initial'|'allChunks'|'allAssets',
    fileBlacklist: [/.svg/] // 资源黑名单
    fileWhitelist: [/.script/] // 资源白名单
  })
]
复制代码

PreloadWebpackPlugin 配置总体上比较简单,需要注意的是 include 属性。该属性默认取值asyncChunks,表示仅预加载异步 js 模块;如果需要预加载图片、字体等资源,则需要将其设置为allAssets,表示处理所有类型的资源。但一般情况下我们不希望把预加载范围扩得太大,所以需要通过fileBlacklistfileWhitelist 进行控制。

对于异步加载的模块,还可以通过webpack内置的/_ webpackPreload: true _/标记进行更细粒度的控制。

以下面的代码为例,webpack会生成标签添加到html页面头部。(备注:prefetch的配置与preload类似,但无需对as属性进行设置。)

import(/* webpackPreload: true */ 'AsyncModule');
复制代码

Prerender

<link rel=“prerender”> 要求浏览器加载 URL 并将其渲染在不可见选项卡中。当用户单击指向该 URL 的链接时,该页面可以立即渲染。如果确定用户下一步将访问特定页面,并且希望更快地渲染出该页面,那么这将非常有用。

<link rel="prerender" href="https://css-tricks.com">
复制代码

这就像在隐藏选项卡中打开 URL,会下载所有资源、创建DOM、布局页面、应用CSS、执行JavaScript等。如果用户导航到指定的href,则隐藏页面将切换到视图中,使其看起来立即加载。Google Search多年来一直以Instant Pages的名称提供这一功能。应该在确定用户将单击该链接的时候才使用 prerender,否则客户端将毫无理由地下载呈现页面所需的所有资产,这会消耗不必要的内存和 CPU 计算资源。

注意,浏览器无需遵循<link rel=“prerender”>的说明。这意味着它可以决定在内存不足或连接速度较慢时,不执行预渲染

浏览器资源加载优先级规则

基本顺序

浏览器首先会按照资源默认的优先级确定加载顺序:

  • html, css, font 这三种类型的资源优先级最高;
  • 然后是 preload 资源(通过 <link rel=“preload"> 标签预加载)
  • 接着是图片、语音、视频;
  • 最低的是prefetch预读取的资源。

资源优先级提升

浏览器会按照如下规则,对优先级进行调整:

  • 对于XHR请求资源:将同步 XHR 请求的优先级调整为最高。

  • 对于图片资源:会根据图片是否在可见视图之内来改变优先级。图片资源的默认优先级为 Low 。现代浏览器为了提高用户首屏的体验,在渲染时会计算图片资源是否在首屏可见视图之内,在的话,会将这部分视口可见图片 ( Image in viewport ) 资源的优先级提升为 High 。

  • 对于脚本资源:浏览器会将根据脚本所处的位置和属性标签分为三类,分别设置优先级。

    • 首先,对于添加 defer / async 属性标签的脚本的优先级会全部降为 Low 。

    • 然后,对于没有添加该属性的脚本,根据该脚本在文档中的位置是在浏览器展示的第一张图片之前还是之后,又可分为两类。在之前的 ( 标记 early ) 它会被定为High优先级,在之后的 ( 标记 late ) 会被设置为 Medium 优先级。

image.png

总结

前端预加载是很强大的性能优化工具,如果使用得当,能提升前端页面加载和渲染的速度。

然而,如果使用不得当,效果可能会适得其反。例如,过度使用 <link rel=“preload”> 预加载不需要的资源,会占用其他更重要资源的带宽;使用<link rel=“preconnect”> 预先连接过多的第三方域,也会占用过多的 CPU 资源。因此,在使用以上前端预加载特性时,需要在合适场景适当使用。

References:

Preload, prefetch and other tags

preload,prefetch, and priorities in chrome

Prefetching, preloading, prebrowsing

浏览器页面资源加载过程与优化

文章分类
前端
文章标签