在 2017 年,确保网页快速加载的工具箱包括从代码压缩和资源优化到缓存、CDN、代码分割和 Tree Shaking 的所有内容。但是,即使您还不熟悉上述概念,并且不确定如何开始,您也可以只使用几个关键字和谨慎的代码结构来获得巨大的性能提升。
最新的 Web 标准 <link rel="preload">
允许您更快地加载关键资源,将于本月晚些时候在 Firefox 中推出。您已经可以在 Firefox Nightly 或 开发者版 中试用它,同时,这是一个很好的机会来回顾一些基础知识,并深入了解与解析 DOM 相关的性能。
了解浏览器内部发生的事情是每个 Web 开发人员最强大的工具。我们将了解浏览器如何解释您的代码以及它们如何通过推测解析帮助您更快地加载页面。我们将分解 defer
和 async
的工作原理,以及如何利用新的关键字 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
尽管如此,同步脚本阻塞解析器仍然是一个问题。并且并非所有脚本对于用户体验都同等重要,例如用于跟踪和分析的脚本。解决方案?使异步加载这些不太重要的脚本成为可能。
defer
和 async
属性 的引入是为了让开发人员能够告诉浏览器哪些脚本要异步处理。
这两个属性都告诉浏览器,它可以在“后台”加载脚本的同时继续解析 HTML,然后在脚本加载后执行它。这样,脚本下载就不会阻塞 DOM 构建和页面渲染。结果:用户可以在所有脚本完成加载之前看到页面。
defer
和 async
之间的区别在于它们在何时开始执行脚本。
defer
在 async
之前引入。它的执行在解析完全完成之后但 DOMContentLoaded
事件之前开始。它保证脚本将按照它们在 HTML 中出现的顺序执行,并且不会阻塞解析器。
async
脚本在它们完成下载后的第一个机会并在窗口的 load
事件之前执行。这意味着 async
脚本可能(很可能)不会按照它们在 HTML 中出现的顺序执行。这也意味着它们可能会中断 DOM 构建。
无论它们在何处指定,async
脚本都以低优先级加载。它们通常在所有其他脚本之后加载,而不会阻塞 DOM 构建。但是,如果 async
脚本先完成下载,则其执行可能会阻塞 DOM 构建以及随后完成下载的所有同步脚本。
注意:属性 async 和 defer 仅适用于外部脚本。如果没有 src
,则会忽略它们。
preload
如果您想推迟处理某些脚本,async
和 defer
非常有用,但是网页上哪些内容对用户体验至关重要呢?推测解析器很方便,但它们仅预加载少量资源类型并遵循自己的逻辑。总体目标是首先提供 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)
30 条评论