更快构建 DOM:推测解析、异步、延迟和预加载

在 2017 年,确保网页快速加载的工具箱包括从代码压缩和资源优化到缓存、CDN、代码分割和 Tree Shaking 的所有内容。但是,即使您还不熟悉上述概念,并且不确定如何开始,您也可以只使用几个关键字和谨慎的代码结构来获得巨大的性能提升。

最新的 Web 标准 <link rel="preload"> 允许您更快地加载关键资源,将于本月晚些时候在 Firefox 中推出。您已经可以在 Firefox Nightly开发者版 中试用它,同时,这是一个很好的机会来回顾一些基础知识,并深入了解与解析 DOM 相关的性能。

了解浏览器内部发生的事情是每个 Web 开发人员最强大的工具。我们将了解浏览器如何解释您的代码以及它们如何通过推测解析帮助您更快地加载页面。我们将分解 deferasync 的工作原理,以及如何利用新的关键字 preload

构建块

HTML 描述了网页的结构。为了理解 HTML,浏览器首先必须将其转换为它们可以理解的格式——文档对象模型 或 DOM。浏览器引擎有一段特殊的代码称为解析器,用于将数据从一种格式转换为另一种格式。HTML 解析器将数据从 HTML 转换为 DOM。

在 HTML 中,嵌套定义了不同标签之间的父子关系。在 DOM 中,对象以树形数据结构链接,捕获这些关系。每个 HTML 标签都由树的一个节点(DOM 节点)表示。

浏览器逐个构建 DOM。一旦第一批代码进入,它就开始解析 HTML,将节点添加到树结构中。

DOM 有两个作用:它是 HTML 文档的对象表示,并且充当连接页面与外部世界(如 JavaScript)的接口。当您调用 document.getElementById() 时,返回的元素是一个 DOM 节点。每个 DOM 节点都有许多函数可用于访问和更改它,用户看到的内容也会相应更改。

网页上找到的 CSS 样式映射到 CSSOM——CSS 对象模型。它与 DOM 非常相似,但适用于 CSS 而不是 HTML。与 DOM 不同,它不能增量构建。由于 CSS 规则可以相互覆盖,因此浏览器引擎必须进行复杂的计算才能确定 CSS 代码如何应用于 DOM。

 

<script> 标签的历史

当浏览器正在构建 DOM 时,如果它在 HTML 中遇到 <script>...</script> 标签,则必须立即执行它。如果脚本是外部的,则必须先下载脚本。

在过去,为了执行脚本,必须暂停解析。只有在 JavaScript 引擎执行完脚本中的代码后,它才会重新开始。

为什么解析必须停止?好吧,脚本可以更改 HTML 及其产品——DOM。脚本可以通过使用 document.createElement() 添加节点来更改 DOM 结构。为了更改 HTML,脚本可以使用臭名昭著的 document.write() 函数添加内容。它臭名昭著,因为它可以以可能影响进一步解析的方式更改 HTML。例如,该函数可以插入一个开始注释标签,使其余 HTML 无效。

脚本还可以查询有关 DOM 的某些信息,如果在 DOM 仍在构建过程中发生这种情况,则可能会返回意外的结果。

document.write() 是一个遗留函数,它可以以意想不到的方式破坏您的页面,因此您不应该使用它,即使浏览器仍然支持它。出于这些原因,浏览器开发了复杂的技术来解决由脚本阻塞引起的性能问题,我将很快解释这些问题。

CSS 怎么样?

JavaScript 阻止解析,因为它可以修改文档。CSS 不能修改文档,所以看起来它没有理由阻止解析,对吧?

但是,如果脚本请求尚未解析的样式信息会怎样?浏览器不知道脚本将要执行什么——它可能会请求 DOM 节点的 background-color 之类的东西,这取决于样式表,或者它可能希望直接访问 CSSOM。

因此,CSS 可能会根据文档中外部样式表和脚本的顺序阻止解析。如果文档中外部样式表放置在脚本之前,则 DOM 和 CSSOM 对象的构建可能会相互干扰。当解析器到达一个脚本标签时,DOM 构建在 JavaScript 完成执行之前无法继续,而 JavaScript 在 CSS 下载、解析以及 CSSOM 可用之前无法执行。

需要注意的另一点是,即使 CSS 不阻止 DOM 构建,它也会阻止渲染。在浏览器同时拥有 DOM 和 CSSOM 之前,它不会显示任何内容。这是因为没有 CSS 的页面通常是不可用的。如果浏览器向您显示了一个没有 CSS 的凌乱页面,然后过了一会儿变成了一个有样式的页面,那么内容的移动和突然的视觉变化会造成混乱的用户体验。

这种糟糕的用户体验有一个名字——未设置样式内容的闪烁或 FOUC。

为了解决这些问题,您应该尽量尽快提供 CSS。还记得流行的“顶部样式,底部脚本”最佳实践吗?现在您知道它为什么存在了!

回到未来——推测解析

每当遇到脚本时暂停解析器意味着您加载的每个脚本都会延迟发现 HTML 中链接的其余资源。

例如,如果您有一些脚本和图像要加载——

<script src="slider.js"></script>
<script src="animate.js"></script>
<script src="cookie.js"></script>
<img src="slide1.png">
<img src="slide2.png">

–过去的过程是这样的

 

大约在 2008 年,IE 引入了一种名为“前瞻下载器”的东西时,这种情况发生了变化。这是一种在执行同步脚本时继续下载所需文件的方法。Firefox、Chrome 和 Safari 很快也效仿,如今大多数浏览器都以不同的名称使用此技术。Chrome 和 Safari 具有“预加载扫描器”,而 Firefox 具有推测解析器。

其思想是:即使在执行脚本时构建 DOM 不安全,您仍然可以解析 HTML 以查看需要检索哪些其他资源。发现的文件将添加到列表中,并开始在并行连接上后台下载。到脚本执行完毕时,文件可能已经下载完毕了。

上面示例的瀑布图现在看起来更像是这样

以这种方式触发的下载请求称为“推测性”请求,因为脚本仍然可能更改 HTML 结构(还记得 document.write 吗?),从而导致浪费的猜测。虽然这种情况可能发生,但并不常见,这就是为什么推测解析仍然能带来巨大的性能提升的原因。

虽然其他浏览器仅以这种方式预加载链接的资源,但在 Firefox 中,HTML 解析器还会推测性地运行 DOM 树构建算法。好处是,当推测成功时,无需重新解析文件的一部分即可实际组合 DOM。缺点是,如果推测失败,则会损失更多工作。

(预)加载内容

这种资源加载方式可以带来显著的性能提升,您无需执行任何特殊操作即可利用它。但是,作为 Web 开发人员,了解推测解析的工作原理可以帮助您充分利用它。

可以预加载的内容集在不同浏览器之间有所不同。所有主要浏览器都预加载

  • 脚本
  • 外部 CSS
  • 以及 <img> 标签中的图像

Firefox 还预加载视频元素的 poster 属性,而 Chrome 和 Safari 预加载内联样式中的 @import 规则。

浏览器可以并行下载的文件数量有限制。限制在不同浏览器之间有所不同,并且取决于许多因素,例如您是否从一个或多个不同的服务器下载所有文件,以及您是否使用 HTTP/1.1 或 HTTP/2 协议。为了尽快渲染页面,浏览器通过为每个文件分配优先级来优化下载。为了确定这些优先级,它们遵循基于资源类型、标记中的位置和页面渲染进度等因素的复杂方案。

在进行推测解析时,浏览器不会执行内联 JavaScript 块。这意味着它不会发现任何脚本注入的资源,并且这些资源很可能在获取队列中的最后位置。

var script = document.createElement('script');
script.src = "//somehost.com/widget.js";
document.getElementsByTagName('head')[0].appendChild(script);

您应该让浏览器能够尽快访问重要资源。您可以将它们放在 HTML 标签中,或者将加载脚本内联并在文档的开头包含它。但是,有时您希望某些资源稍后加载,因为它们不太重要。在这种情况下,您可以通过在文档的末尾使用 JavaScript 加载它们来将其隐藏在推测解析器之外。

您还可以查看有关如何优化页面以进行推测解析的 MDN 指南

defer 和 async

尽管如此,同步脚本阻塞解析器仍然是一个问题。并且并非所有脚本对于用户体验都同等重要,例如用于跟踪和分析的脚本。解决方案?使异步加载这些不太重要的脚本成为可能。

deferasync 属性 的引入是为了让开发人员能够告诉浏览器哪些脚本要异步处理。

这两个属性都告诉浏览器,它可以在“后台”加载脚本的同时继续解析 HTML,然后在脚本加载后执行它。这样,脚本下载就不会阻塞 DOM 构建和页面渲染。结果:用户可以在所有脚本完成加载之前看到页面。

deferasync 之间的区别在于它们在何时开始执行脚本。

deferasync 之前引入。它的执行在解析完全完成之后但 DOMContentLoaded 事件之前开始。它保证脚本将按照它们在 HTML 中出现的顺序执行,并且不会阻塞解析器。

async 脚本在它们完成下载后的第一个机会并在窗口的 load 事件之前执行。这意味着 async 脚本可能(很可能)不会按照它们在 HTML 中出现的顺序执行。这也意味着它们可能会中断 DOM 构建。

无论它们在何处指定,async 脚本都以低优先级加载。它们通常在所有其他脚本之后加载,而不会阻塞 DOM 构建。但是,如果 async 脚本先完成下载,则其执行可能会阻塞 DOM 构建以及随后完成下载的所有同步脚本。

注意:属性 async 和 defer 仅适用于外部脚本。如果没有 src,则会忽略它们。

preload

如果您想推迟处理某些脚本,asyncdefer 非常有用,但是网页上哪些内容对用户体验至关重要呢?推测解析器很方便,但它们仅预加载少量资源类型并遵循自己的逻辑。总体目标是首先提供 CSS,因为它会阻塞渲染。同步脚本始终比异步脚本具有更高的优先级。视口中可见的图像应在折线以下的图像之前下载。还有字体、视频、SVG……总之——很复杂。

作为作者,您知道哪些资源对于渲染您的页面最重要。其中一些资源通常隐藏在 CSS 或脚本中,浏览器可能需要相当长的时间才能发现它们。对于这些重要资源,您现在可以使用 <link rel="preload"> 来告诉浏览器您希望尽快加载它们。

您只需编写

<link rel="preload" href="very_important.js" as="script">

您可以链接几乎任何内容,并且 as 属性告诉浏览器它将要下载什么。一些可能的值是

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

您可以在 MDN 上查看其余内容类型。

字体可能是隐藏在 CSS 中的最重要内容。它们对于渲染页面上的文本至关重要,但只有在浏览器确定要使用它们时才会加载。该检查仅在 CSS 已解析和应用,并且浏览器已将 CSS 规则与 DOM 节点匹配后才会发生。这发生在页面加载过程的相当后期,并且通常会导致文本渲染不必要的延迟。您可以通过在链接字体时使用 preload 属性来避免这种情况。

在预加载字体时需要注意的一件事是,即使字体在同一域名上,您也必须设置 <a href="https://mdn.org.cn/en-US/docs/Web/HTTP/Access_control_CORS" target="_blank" rel="noopener">crossorigin</a> 属性

<link rel="preload" href="font.woff" as="font" crossorigin>

preload 功能目前支持有限,因为浏览器仍在推出它,但您可以在 此处 检查进度。

结论

浏览器是复杂的野兽,自 90 年代以来一直在发展。我们已经介绍了其中一些遗留的怪癖以及 Web 开发中的一些最新标准。按照这些指南编写代码将帮助您为提供流畅的浏览体验选择最佳策略。

如果您渴望了解更多关于浏览器如何工作的信息,以下是一些您应该查看的其他 Hacks 文章

Quantum 近距离观察:什么是浏览器引擎?
超快速 CSS 引擎内部:Quantum CSS(又名 Stylo)

关于 Milica Mihajlija

Milica Mihajlija 的更多文章…


30 条评论

  1. Jerry

    很棒的文章,图像预加载也是提高性能的关键,并且要理解每个图像都需要向网站发起请求。CSS Grid 布局也可以帮助正确加载页面。

    2017 年 9 月 14 日 09:52

  2. Victor

    读得不错。

    2017 年 9 月 14 日 09:54

  3. Wellington Torrejais da Silva

    不错!

    2017 年 9 月 14 日 11:06

  4. Ardil

    好文章!
    以前从未想过这些事情。

    2017 年 9 月 14 日 11:18

  5. Leon

    很棒的文章。随着现代 Web 应用程序的复杂性,开发人员对网站执行方式的深入理解变得越来越重要。

    2017 年 9 月 14 日 15:23

  6. sankar mookerjee

    不错的文章。

    2017 年 9 月 14 日 17:45

  7. 李艳

    非常好的文章,清楚地说明了重要的事情。

    2017 年 9 月 14 日 19:00

  8. RodM

    我对 https://mdn.org.cn/en-US/docs/Web/HTML/Preloading_content 上的“视频预加载示例”有点困惑。

    https://mdn.org.cn/en-US/docs/Web/HTML/Preloading_content

    它似乎正在预加载同一视频的两种不同文件类型,其中只使用其中一种。因此,其中一个预加载会消耗带宽,然后被丢弃。

    此外,我还担心许多自动播放视频的网站。虽然 Firefox 允许您偏好关闭自动播放,但这些网站可能会决定预加载视频,即使它们没有播放,也会消耗带宽。

    2017 年 9 月 14 日 19:20

    1. Milica Mihajlija

      嗨,Rod,感谢您的反馈!MDN 示例只是展示了类型检查,但它已更新为考虑带宽使用情况。至于预加载自动播放视频的影响,这是一个合理的担忧,但希望开发人员会在实际使用案例中考虑性能的所有方面。

      2017 年 9 月 22 日 04:57

  9. zhaoxieluoke

    很棒的文章。我喜欢它。

    2017 年 9 月 14 日 20:01

  10. zhaoxieluoke

    很棒的文章

    2017 年 9 月 14 日 20:03

  11. Mohamed hussain

    不错的文章,图片清楚地解释了正在发生的事情……

    2017 年 9 月 14 日 22:21

  12. Nishant

    优秀的文章。

    2017 年 9 月 15 日 00:19

  13. tinku

    优秀的文章!非常感谢。

    2017 年 9 月 15 日 00:44

  14. Lalit

    优秀的文章。之前从未意识到 DOM 创建的所有方面。谢谢!

    2017 年 9 月 15 日 01:13

  15. Dickriven Chellemboyee

    不错的文章,所以很快就会使用预加载来加载重要的资源。

    2017 年 9 月 15 日 02:12

  16. gusamasan

    如果我把所有 CSS、SCRIPT(按此顺序)都放到 HEAD 标签中会发生什么?

    通常,我使用 BODY 标签的“onLoad”属性调用脚本。这是一个坏主意吗?

    2017 年 9 月 15 日 06:21

    1. Milica Mihajlija

      如果在 head 标签中,CSS 在脚本之前包含,则可以在标题为“解析器阻塞 CSS”的图表上看到可能的情况。

      使用“onload”执行脚本发生在 load 事件之后,这意味着在所有内容(包括图像、字体等)加载完成后。这并不坏,但根据脚本的不同,它可能不是最佳选择。如果您的脚本正在操作 DOM,则可以使用 DOMContentLoaded 事件来更快地执行脚本,而无需等待所有依赖资源加载完成。

      2017 年 9 月 15 日 07:43

  17. Stimpygato

    这是一篇写得非常好的文章,在开发时很好地传达了一些重要的性能概念。

    2017 年 9 月 15 日 08:34

  18. Frederik

    > 预加载字体时需要注意的一件事是,即使字体在同一域名下,也必须设置 crossorigin 属性。

    这是为什么?

    2017 年 9 月 15 日 10:40

    1. Milica Mihajlija

      这是因为 CSS 规范要求字体以 匿名模式 CORS 方式获取。

      以下是一些关于此的更多上下文:https://github.com/w3c/preload/issues/32

      2017 年 9 月 15 日 10:54

  19. pranabjyoti

    我非常喜欢这篇文章。

    2017 年 9 月 16 日 17:34

  20. andy

    你好,
    非常棒且简洁的文章。
    你能告诉我你在哪里/如何构建这些图表的?

    2017 年 9 月 17 日 23:50

    1. Milica Mihajlija

      谢谢!我使用 Sketch 和 After Effects 进行动画制作。

      2017 年 9 月 21 日 13:02

  21. Kartar

    不错的文章

    2017 年 9 月 19 日 18:47

  22. D D

    非常有帮助的文章。我感谢对现有技术的回顾,它超越了宣传这项新功能,并为何时使用现有技术来实现相同目标(加载性能)提供了良好的建议。非常感谢!

    也就是说,回顾中包含所有这些漂亮的瀑布图。是否可以再添加一个关于 的图表?

    (好的,所以要求添加一个新图表是一个很大的要求。不过,我希望能够更实际地了解其工作原理,因此我尝试解释了它听起来是如何工作的。也许您能告诉我这是否正确?)

    我假设(例如对于 Web 字体)获取会立即在开始时发生,与“推测性解析为必需”的资产并行……然后在 CSS 和 DOM 加载到足以指定字体时立即显示。这是正确的吗?(与其他技术的图表相比,我需要更多阅读和推理才能了解这一点!)

    总的来说,我不能过分强调:再次感谢您撰写了这篇文章,它通俗易懂,对初学者友好。我将在我的网站上尝试一些这些技术!必须热爱 Hacks 博客!

    2017 年 9 月 25 日 09:29

    1. Milica Mihajlija

      很高兴听到这些,谢谢!您关于预加载如何工作的推理是正确的,并且事后看来,另一个图表在那里会很方便。现在有点晚了,但我建议任何有兴趣进一步探索此问题的人加载一些使用预加载的页面,并在 Dev Tools 的 Network 选项卡中查看瀑布图。这是一篇 非常好的博文,可以了解有关字体加载和网络请求的更多信息,并且一个开始学习示例的好地方是 MDN

      2017 年 9 月 27 日 04:22

  23. Murat

    感谢您撰写这篇文章,我学到了很多有趣的东西。

    2017 年 9 月 27 日 01:41

  24. Brian Ball

    文章构思巧妙。

    我将分享这篇文章,作为简洁写作的示例,它不会让人陷入故事中——就像一些优秀的作家想要做的那样。

    信息:已交付 谢谢!

    2017 年 9 月 30 日 11:06

  25. Motasem Aghbar

    很棒的文章,非常有用的信息!

    2017 年 10 月 1 日 05:58

本文的评论已关闭。