HTML - 预解析, async/defer 和 preload

本文是关于 mactavish 学弟在众成翻译上翻译的文章 更快地构建 DOM: 使用预解析, async, defer 以及 preload ★ Mozilla Hacks – the Web developer blog 的阅读笔记。特此感谢。

Parsing

首先我们需要了解浏览器正常的文档解析流程(Parsing):

  • 浏览器引擎的解析器会将 HTML 转换成 DOM(解析,DOM 的构建)
  • 在解析 HTML 字符串的过程中,DOM 节点逐个被添加到树中
  • 在 DOM 中,对象被关联在树中用户捕获标签之间的父子关系
  • CSS 样式被映射到 CSSOM
    • CSS 规则会互相覆盖,所以浏览器引擎需要进行复杂计算,以确定 CSS 代码如何应用到 DOM
    • CSS 可能会阻塞解析:
      • 当解析器获取一个 script 标签时,DOM 将等待 JavaScript 执行完毕后继续构建;
      • 若有 CSS 样式表先于该标签,则该标签的 JavaScript 代码将等待 CSS 下载,解析,且 CSSOM 可以使用时,才会执行
    • CSS 会阻塞 DOM 的渲染:直到 DOM 和 CSSOM 准备好之前,浏览器什么都不会显示。
      • FOUC - Flash of Unstyled Content: 没有任何样式的页面突然变换成了有样式的。

CSSOM

CSS Object Model is a set of APIs allowing to manipulate CSS from JavaScript…It allows to read and modify CSS style dynamically.

一系列的可以操作 CSS 样式的 JS 方法。

CSS.supports

@supports CSS at-rule

@supports (display: flex) {
  div {
    display: flex;
  }
}

@supports not (display: flex) {
  div {
    float: right;
  }
}

@supports (display: flexbox) and (not (display: inline-grid)) {
  /* blah ... */
}

预解析 Speculative Parsing

它的概念是:虽然在执行脚本时构建 DOM 是不安全的,但是你仍然可以解析 HTML 来查看其它需要检索的资源。找到的文件会被添加到一个列表里并开始 在后台并行地下载 。当脚本执行完毕之后,这些文件很可能已经下载完成了。

说人话:就是会提前的下载 HTML 页面上声明了的脚本…

下载是并行的,执行是先后的,以这种方式触发的下载请求称为”预测“,因为很有可能脚本还是会改变 HTML 结构,导致预测的浪费(即下载的脚本是无效的,并不会被执行),但这并不常见,所以预解析仍然可以带来很大的性能提升。

  • 预加载的内容:
    • 脚本
    • 外部 CSS
    • 来自 img 标签的图片
    • Firefox 预加载 video 元素 poster 属性
    • Chrome/Safari 预加载 @import 内联样式
  • 预加载会受到浏览器并行下载文件的数量限制(HTTP 1.x),如 Chrome 浏览器最多只能并行下载6个文件。
  • 预解析时,浏览器不会执行内联的 JS 代码块。
  • 用 JS 加载不那么重要的内容来避免预解析,这里的意思是说,页面上某些不重要的脚本,相比于其他必需脚本来说,是应该 迟下载,迟执行 的,通过用 JS 动态加载它们,它们就不会一开始出现在 HTML 文档的 script 标签中,则浏览器的预解析不会处理它们(相对的就会更优先的解析必需脚本)

MDN 的文档 中提到:

  • Firefox 4 及之后的版本的 HTML 解析器支持在预解析时不使用浏览器的主线程,从而可以达到不阻塞正常 DOM 构建和渲染的效果。
  • 尽量确保预加载成功:如果使用了 base 元素指定页面加载资源的根地址(base URI),则确保不要用 JS 去加载该元素(注:放到 head 标签里就可以了)
  • 避免预解析构建的 DOM 树失效,这里需要避免一些对 document.write 方法的滥用,具体可以看 MDN 文档中 “Avoid losing tree builder output” 一节中列出的几个点

defer 和 async

  • deferasync 在下载时都不会阻塞 HTML 解析器的执行(解析 DOM)

  • deferasync 之间的不同是他们开始执行脚本的时机的不同

  • defer 脚本在 HTML 文档解析完全完成之后才开始执行,处在 DOMContentLoaded 事件之前。保证脚本会按照它在 HTML 中出现的顺序执行,且不会阻塞解析

    • 但根据红宝书的说法,在一个页面中最好只有一个带 defer 的脚本;保险起见,在 DOMContentLoaded 的事件回调中执行实际的业务代码:

    HTML5规范要求脚本按照它们出现的先后顺序执行,因此第一个延迟脚本会先于第二个延迟脚本执行,而这两个脚本会先于DOMContentLoaded事件执行。在现实当中,延迟脚本并不一定会按照顺序执行,也不一定会在 DOMContentLoaded 事件触发前执行,因此最好只包含一个延迟脚本。

    • 红宝书的说法是有实锤的,在 这个issue中 有人做了实验确认了 Firefox 在处理 defer 属性脚本时,脚本的代码是在 DOMContentLoaded 事件后才执行的;同时,在 IE 9 及以下版本时,defer 脚本的顺序执行也不能保证。
  • async 脚本在它们完成下载后的第一时间执行,它处在 window 的 load 事件之前,有可能(并且很有可能)设置了 async 的脚本不会按照它们在 HTML 中出现的顺序执行,它们可能会中断 DOM 的构建。

    • 多个 async 脚本之前的执行顺序不总是它们的出现顺序,而取决于 哪个文件先完成下载
    • 经典使用场景:不依赖于 DOM 的第三方脚本,例如 Google Analytics
    • 引用 CSS Tricks 中的描述:

    If you use the async attribute, you are saying: I don’t want the browser to stop what it’s doing while it’s downloading this script.

    Dynamically inserted scripts execute asynchronously by default, so to turn on synchronous execution (i.e. scripts execute in the order they were inserted) set async to false

  • 没有 defer 或者 async 属性的脚本,包括行内脚本(inline scripts)会(在 HTML 解析到时)立刻下载并执行,之后浏览器继续解析页面(文档)「注:即阻塞解析」

  • 若同时使用 deferasync,则以 async 为准,当浏览器不支持 async 时,回退到 defer

  • 兼容性:

    • defer 在 IE6-9 中的实现有问题(并不按规范实现),IE10+ 完整支持
    • async IE10+
  • 一张图直观的表达:

preload

使用 link 元素的 preload 属性告知浏览器对资源进行预先的加载。例如预加载一个脚本文件,设置 href 为脚本路径,as 属性为 “script”。

as 属性的可能的值有:

  • script
  • style
  • image
  • font
  • audio
  • video

预加载字体需要设置 crossorigin 属性,即使字体在同一个域名下。

preload 目前仅在部分新版本的 Chrome 和 Firefox 上得到支持。

More?

其实还有一个问题:如果有一部分 CSS 是在页面所有的 HTML 加载结束之后再被动态插入,此时的 CSSOM 是否会被重新构建?如果同时又有 JS 脚本需要执行,其先后顺序如何?

所以有空的话,顺便也了解下浏览器运作机制呗:

Reference