<link>标签实现预加载功能

4,004 阅读9分钟

前言

最近在研究 vue-cli 3.0生成的工程,在构建后生成的 index.html里面发现了下面这种用法:


<link as=style href=/css/app.f60416c7.css rel=preload>
<link as=script href=/js/app.69189fdd.js rel=preload>

这就触到了本人的知识盲区了,本着扫盲的目的,研究了下 link 标签,发现这个小东西功能还是挺强大的,上面的就是为了实现预加载功能,懂点儿英文的,一看见preload 就大致知道了。


之前也有预加载技术,像 prefetch,subresource 等,关于这两者和 preload 的区别,这是另外的话题了, 感兴趣的可以自己搜一下,不想搜的,你只要知道这两个跟 preload 相比弱的一逼就行了,就是 prefetch 浏览器兼容性方面稍微好一点点,这三个也各有偏重和应用场景,就不详细介绍了,下面我们就详细展开preload 这块。

功能介绍:

preload 是一项新的 web 标准,旨在提高性能,让 FE 对加载的控制更加粒度化。它让开发者有自定义加载逻辑的能力,免受基于脚本的加载器所带来的性能损耗。

preload 一个基本的用法就是提前加载资源,尽管大多数基于标记语言的资源能被浏览器的预加载器(preloader)尽早发现,但不是所有的资源都是基于标记语言的,比如一些隐藏在 css 和 js 中的资源(字体,图片等),当浏览器发现页面需要这些资源时,重新走一遍加载执行渲染的过程,会降低用户体验,并且对页面的渲染 造成延迟;


Preloader 简介
HTML 解析器在创建 DOM 时如果碰上同步脚本(synchronous script),解析器会停止创建 DOM,转而去执行脚本。所以,如果资源的获取只发生在解析器创建 DOM时,同步脚本的介入将使网络处于空置状态,尤其是对外部脚本资源来说,当然,页面内的脚本有时也会导致延迟。

预加载器(Preloader)的出现就是为了优化这个过程,预加载器通过分析浏览器对 HTML 文档的早期解析结果(这一阶段叫做“令牌化(tokenization)”),找到可能包含资源的标签(tag),并将这些资源的 URL 收集起来。令牌化阶段的输出将会送到真正的 HTML 解析器手中,而收集起来的资源 URLs 会和资源类型一起被送到读取器(fetcher)手中,读取器会根据这些资源对页面加载速度的影响进行有次序地加载。


预加载的好处:

  1. 让浏览器提前加载指定资源(这里预加载完成后并不执行),在需要执行的时候在执行,这样将加载和执行分开,可以不阻塞渲染和 window.onload事件。
  1. 提前预加载指定资源,特别是字体文件,不会再出现 font 字体在页面渲染出来后,才加载完毕,然后页面字体闪一下变成预期字体。
  1. 带有 onload 事件,可以自定义资源在预加载完毕后的回调函数。

涉及属性介绍:

应用场景:

  • 包含媒体

<link>元素有一个很棒的特性是它们能够接受一个media属性。它们可以接受媒体类型或有效的媒体查询作为属性值,这将令你能够使用响应式的预加载!

让我们来看一个简单的示例(可以查看Github上的源代码在线示例):

<head>
  <meta charset="utf-8">
  <title>Responsive preload example</title>

  <link rel="preload" href="bg-image-narrow.png" as="image" media="(max-width: 600px)">
  <link rel="preload" href="bg-image-wide.png" as="image" media="(min-width: 601px)">

  <link rel="stylesheet" href="main.css">
</head>
<body>
  <header>
    <h1>My site</h1>
  </header>

  <script>
    var mediaQueryList = window.matchMedia("(max-width: 600px)");
    var header = document.querySelector('header');

    if(mediaQueryList.matches) {
      header.style.backgroundImage = 'url(bg-image-narrow.png)';
    } else {
      header.style.backgroundImage = 'url(bg-image-wide.png)';
    }
  </script>
</body>

你可以看到我们在<link>元素中包含了一个media属性,因此,当用户在使用较窄屏幕的设备时,较窄的图片将会被预加载,而在较宽的设备上,较宽的图片将被预加载。然后我们仍需要在header元素上附加合适的图片——通过Window.matchMedia / MediaQueryList 来加以实现(可以查看 Testing media queries一文来了解更多信息)。


  • 字体提前加载

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


<link rel=preload href='font.woff2' as=font type='font/woff2' crossorigin />


NOTE :
crossorigin 属性在加载字体的时候是必须的,即便字体没有跨域是在自己公司的服务器上,因为用户代理必须采用匿名模式来获取字体资源(为什么会这样呢?)。
type 属性可以确保浏览器只获取自己支持的资源。



  • 动态加载,但不执行

另外一个有意思的场景也因为 preload 的出现变得可能——当你想加载某一资源但却不想执行它。比如说,你想在页面生命周期的某一时刻执行一段脚本,而你无法对这段脚本做任何修改,不可能为它创建一个所谓的 runNow()函数。


在 preload 出现之前,你能做的很有限。如果你的方法是在希望脚本执行的位置插入脚本,由于脚本只有在加载完成以后才能被浏览器执行,也就是说你得等上一会儿。如果采用 XHR 提前加载脚本,浏览器会拒绝重用这段脚本,有些情况下,你可以使用 eval 函数来执行这段脚本,但该方法并不总是行得通,也不是完全没有副作用。


现在有了 preload,一切变得可能


var link = document.createElement("link");
link.href = "myscript.js";
link.rel = "preload";
link.as = "script";
document.head.appendChild(link);


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


var 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 async>虽好,但却会阻塞 window 的 onload 事件。某些情况下,你可能希望这样,但总有一些情况你不希望阻塞 window 的 onload 。


举个例子,你想尽可能快的加载一段统计页面访问量的代码,但又不愿意这段代码的加载给页面渲染造成延迟从而影响用户体验,关键是,你不想延迟 window 的 onload 事件。


有了preload, 分分钟搞定。


<link rel="preload" as="script" href="async_script.js"
      onload="var script = document.createElement('script'); script.src = this.href; document.body.appendChild(script);">



  • 响应式加载

preload 是一个link,根据规范有一个media 属性(现在 Chrome 还不支持,不过快了),该属性使得选择性加载成为可能。


有什么用处呢?假设你的站点同时支持桌面和移动端的访问,在使用桌面浏览器访问时,你希望呈现一张可交互的大地图,而在移动端,一张较小的静态地图就足够了。


你肯定不想同时加载两个资源,现在常见的做法是通过 JS 判断当前浏览器类型动态地加载资源,但这样一来,浏览器的预加载器就无法及时发现他们,可能耽误加载时机,影响用户体验和 SpeedIndex 评分。


怎样才能让浏览器尽可能早的发现这些资源呢?还是 Preload!


通过 Preload,我们可以提前加载资源,利用 media 属性,浏览器只会加载需要的资源。


<link rel="preload" as="image" href="map.png" media="(max-width: 600px)">
<link rel="preload" as="script" href="map.js" media="(min-width: 601px)">



  • http header 实现预加载

Preload 还有一个特性是其可以通过 HTTP 头信息被呈现。也就是说上文中大多数的基于标记语言的声明可以通过 HTTP 响应头实现。(唯一的例外是有 onload 事件的例子,我们不可能在 HTTP 头信息中定义事件处理函数。)


Link: <thing_to_load.js>;rel="preload";as="script"
Link: <thing_to_load.woff2>;rel="preload";as="font";crossorigin


这一方式在有些场景尤其有用,比如,当负责优化的人员与页面开发人员不是同一人时(也就是说优化人员可能无法或者不想修改页面代码),还有一个杰出的例子是外部优化引擎(External optimization engine),该引擎对内容进行扫描并优化。



  • 浏览器特性检查

前面所有的列子都基于一种假设——浏览器一定程度上支持 preload,至少实现了脚本和样式加载等基本功能。但如果这个假设不成立了。一切都将是然并卵。


为了判断浏览器是否支持 preload,我们修改了 DOM 的规范从而能够获知 rel 支持那些值(是否支持 rel=‘preload’)。


至于如何进行检查,原文中没有,但 Github有一段代码可供参考。


var DOMTokenListSupports = function(tokenList, token) {
  if (!tokenList || !tokenList.supports) {
    return;
  }
  try {
    return tokenList.supports(token);
  } catch (e) {
    if (e instanceof TypeError) {
      console.log("The DOMTokenList doesn't have a supported tokens list");
    } else {
      console.error("That shouldn't have happened");
    }
  }
};

var linkSupportsPreload = DOMTokenListSupports(document.createElement("link").relList, "preload");
if (!linkSupportsPreload) {
  // Dynamically load the things that relied on preload.
}


浏览器兼容性:

caniuse.com 网站上显示浏览器版本支持情况如下,目前还是比较高版本的浏览器会支持此功能,不过大家也不要担心,在不支持的浏览器环境中,这部分标签会被忽略,可以做到平稳降级。