更快地构建 DOM: 使用预解析, async, defer 以及 preload

2017/09/28 · JavaScript
· async,
defer,
DOM,
preload

原文出处: Milica
Mihajlija   译文出处:众成翻译   


2017年,保证页面快速加载的手段涵盖了方方面面,从压缩和资源优化,到缓存,CDN,代码分割以及
tree shaking 等。
然而,即便你不熟悉上面的这些概念,或者你感到无从下手,你仍然可以通过几个关键字以及精细的代码结构使得你的页面获得巨大的性能提升。

新的 Web 标准 “使你能够更快地加载关键资源,这个月晚些时候,Firefox
就会支持这个特性。同时在 Firefox
Nightly
版本或者 开发者版本
上已经可以使用这些功能。与此同时,这也是回顾基本原理,深入了解 DOM
解析相关性能的一个好时机。

理解浏览器的内部机制是每个 web
开发者最强有力的工具。我们看看浏览器是如何解释代码以及如何使用推测解析(speculative
parsing)来帮助页面快速加载的。我们会分析 deferasync
是如何生效的以及如何利用新的关键字 preload

浏览器的渲染:过程与原理

2017/10/18 · 基础技术 ·
2 评论 ·
浏览器

原文出处: 天方夜   

 必发88官网 1内内容说明

本文不是关于浏览器渲染的底层原理或前端优化具体细节的讲解,而是关于浏览器对页面的渲染——这一过程的描述及其背后原理的解释。这是因为前端优化是一个非常庞大且零散的知识集合,一篇文章如果要写优化的具体方法恐怕只能做一些有限的列举。

然而,如果了解清楚浏览器的渲染过程、渲染原理,其实就掌握了指导原则。根据优化原则,可以实现出无数种具体的优化方案,各种预编译、预加载、资源合并、按需加载方案都是针对浏览器渲染习惯的优化。

转载自web fundamental

构建模块

HTML 描述了一个页面的结构。为了理解
HTML,浏览器首先会将HTML转换成其能够理解的一种格式 –
文档对象模型(Document Object
Model)
或者简称为 DOM。
浏览器引擎有这么一段特殊的代码叫做解析器,用来将数据从一种格式转换成另外一种格式。一个
HTML 解析器就能将数据从 HTML 转换到 DOM。

在 HTML 当中,嵌套(nesting)定义了不同标签的父子关系。在 DOM
当中,对象被关联在树(一种数据结构)中用于捕获这些关系。每一个 HTML
标签都对应着树种的某个节点(DOM节点)。

浏览器一个比特一个比特地构建
DOM。一旦第一个代码块加载到浏览器当中,它就开始解析
HTML,添加节点到树中。

必发88官网 2

DOM 扮演着两种角色:它既是 HTML
文档的对象表示,也充当着外界(比如JavaScript)和页面交互的接口。
当你调用 document.getElementById(),返回的元素是一个 DOM 节点。每个
DOM 节点都有很多函数可以用来访问和改变它,用户可以看到相应的变化。

必发88官网 3

页面上的 CSS 样式被映射到 CSSOM 上 – CSS 对象模型(CSS Object
Model)。它就像
DOM,但是只针对于 CSS 而不是 HTML。不像 DOM,它不能增量地构建。因为 CSS
规则会相互覆盖,所以浏览器引擎要进行复杂的计算来确定 CSS 代码如何应用到
DOM 上。

必发88官网 4

关键渲染路径

提到页面渲染,有几个相关度非常高的概念,最重要的是关键渲染路径,其他几个概念都可以从它展开,下面稍作说明。

关键渲染路径(Critical Rendering
Path)
是指与当前用户操作有关的内容。例如用户刚刚打开一个页面,首屏的显示就是当前用户操作相关的内容,具体就是浏览器收到
HTML、CSS 和 JavaScript 等资源并对其进行处理从而渲染出 Web 页面。

了解浏览器渲染的过程与原理,很大程度上是为了优化关键渲染路径,但优化应该是针对具体问题的解决方案,所以优化没有一定之规。例如为了保障首屏内容的最快速显示,通常会提到渐进式页面渲染,但是为了渐进式页面渲染,就需要做资源的拆分,那么以什么粒度拆分、要不要拆分,不同页面、不同场景策略不同。具体方案的确定既要考虑体验问题,也要考虑工程问题。

构建对象模型

浏览器渲染页面前需要先构建 DOM 和 CSSOM 树。因此,我们需要确保尽快将
HTML 和 CSS 都提供给浏览器。

  • 字节 → 字符 → 标记 → 节点 → 对象模型。
  • HTML 标记转换成文档对象模型 (DOM);CSS 标记转换成 CSS 对象模型
    (CSSOM)。DOM 和 CSSOM 是独立的数据结构。
  • Chrome DevTools Timeline可以捕获和检查 DOM 和 CSSOM
    的构建和处理开销。

关于“标签的历史

当浏览器构建 DOM 的时候,如果在 HTML 中遇到了一个
“标签,它必须立即执行。如果脚本是来自于外部的,那么它必须首先下载脚本。

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

必发88官网 5

那位为什么解析必须要暂停呢?那是因为脚本可以改变 HTML以及它的产物 ——
DOM。 脚本可以通过 document.createElement()方法添加节点来改变 DOM
结构。为了改变
HTML,脚本可以使用臭名昭著的document.write()方法来添加内容。它之所以臭名昭著是因为它能以进一步影响
HTML 解析的方式来改变
HTML。比如,该方法可以插入一个打开的注释标签来使得剩余的 HTML
都变得不合法。

必发88官网 6

脚本还可以查询关于 DOM 的一些东西,如果是在 DOM
还在在构建的时候,它可能会返回意外的结果。

必发88官网 7

document.write()
是一个遗留的方法,它能够以预料之外的方式破坏你的页面,你应该避免使用它。处于这些原因,浏览器开发出了一些复杂的方法来应对脚本阻塞导致的性能问题,稍后我会解释。

浏览器渲染页面的过程

从耗时的角度,浏览器请求、加载、渲染一个页面,时间花在下面五件事情上:

  1. DNS 查询
  2. TCP 连接
  3. HTTP 请求即响应
  4. 服务器响应
  5. 客户端渲染

本文讨论第五个部分,即浏览器对内容的渲染,这一部分(渲染树构建、布局及绘制),又可以分为下面五个步骤:

  1. 处理 HTML 标记并构建 DOM 树。
  2. 处理 CSS 标记并构建 CSSOM 树。
  3. 将 DOM 与 CSSOM 合并成一个渲染树。
  4. 根据渲染树来布局,以计算每个节点的几何信息。
  5. 将各个节点绘制到屏幕上。

需要明白,这五个步骤并不一定一次性顺序完成。如果 DOM 或 CSSOM
被修改,以上过程需要重复执行,这样才能计算出哪些像素需要在屏幕上进行重新渲染。实际页面中,CSS
与 JavaScript 往往会多次修改 DOM 和 CSSOM,下面就来看看它们的影响方式。

文档对象模型 (DOM)

<html>
  <head>
    <meta name="viewport" content="width=device-width,initial-scale=1">
    <link href="style.css" rel="stylesheet">
    <title>Critical Path</title>
  </head>
  <body>
    <p>Hello web performance students!</p>
    <div><img src="awesome-photo.jpg"></div>
  </body>
</html>

一个包含一些文本和一幅图片的普通 HTML 页面,浏览器如何处理此页面?

HTML解析器输出的树是由DOM元素和属性节点组成的,它是HTML文档的对象化描述,也是HTML元素与外界(如Javascript)的接口。DOM与标签有着几乎一一对应的关系。

 必发88官网 8 

  1. 转换: 浏览器从磁盘或网络读取 HTML
    的原始字节,并根据文件的指定编码(如 UTF-8)将它们转换成各个字符。
  2. Tokenizing: 浏览器将字符串转换成 W3C HTML5
    标准规定的各种tokens,例如,“<html>”、“<body>”,以及其他尖括号内的字符串。每个token都具有特殊含义和一组规则。
  3. 词法分析: 发出的标记转换成定义其属性和规则的“对象”。
  4. DOM 构建: 最后,由于 HTML
    标记定义不同标记之间的关系(一些标记包含在其他标记内),创建的对象链接在一个树数据结构内,此结构也会捕获原始标记中定义的父项-子项关系:HTML 对象是 body 对象的父项,bodyparagraph对象的父项,依此类推。

整个流程最终输出是页面的文档对象模型
(DOM),浏览器对页面进行的所有进一步处理都会用到它。

浏览器每次处理 HTML
标记时,都会完成以上所有步骤:将字节转换成字符,确定tokens,将tokens转换成节点,然后构建
DOM 树。这整个流程可能需要一些时间才能完成,有大量 HTML
需要处理时更是如此。

 必发88官网 9

如果您打开 Chrome DevTools
并在页面加载时记录时间线,就可以看到执行该步骤实际花费的时间。在上例中,将一堆
HTML 字节转换成 DOM 树大约需要 5
毫秒。对于较大的页面,这一过程需要的时间可能会显著增加。创建流畅动画时,如果浏览器需要处理大量
HTML,这很容易成为瓶颈。

DOM
树捕获文档标记的属性和关系,但并未告诉我们元素在渲染后呈现的外观。那是
CSSOM 的责任。

那么 CSS 会阻塞页面吗 ?

JavaScript 阻塞页面解析是因为它可以修改文档。CSS
不能修改文档,所以看起来它没有理由去阻塞页面解析,对吗?

那么,如果脚本需要样式信息,但样式还没有被解析呢?浏览器并不知道脚本要怎么执行——它可能会需要类似
DOM 节点的background-color
属性,而这个属性又依赖于样式表,或者它期望能够直接访问 CSSOM。

必发88官网 10

正因为如此,CSS
可能会阻塞解析,取决于外部样式表和脚本在文档中的顺序。如果在文档中外部样式表放置在脚本之前,DOM
对象和 CSSOM 对象的构建可以互相干扰。 当解析器获取到一个 script
标签,DOM 将无法继续构建直到 JavaScript 执行完毕,而 JavaScript 在 CSS
下载完,解析完,并且 CSSOM 可以使用的时候,才能执行。

必发88官网 11

另外一件要注意的事是,即使 CSS 不阻塞 DOM 的构建,它也会阻塞 DOM
的渲染。直到 DOM 和 CSSOM
准备好之前,浏览器什么都不会显示。这是因为页面没有 CSS
通常无法使用。如果一个浏览器给你显示了一个没有 CSS
的凌乱的页面,而几分钟之后又突然变成了一个有样式的页面,变换的内容和突然视觉变化使得用户体验变得非常糟糕。

具体可以参考由 Milica (@micikato) 在 CodePen 上制作的例子 —— Flash of
Unstyled Content。

这种糟糕的用户体验有一个名字 — Flash of Unstyled Content 或是 FOUC

为了避免这个问题,你应该尽快地呈现
CSS。记得流行的“样式放顶部,脚本放底部”的最佳实践吗?你现在知道它是怎么来的了!

阻塞渲染:CSS 与 JavaScript

谈论资源的阻塞时,我们要清楚,现代浏览器总是并行加载资源。例如,当 HTML
解析器(HTML Parser)被脚本阻塞时,解析器虽然会停止构建
DOM,但仍会识别该脚本后面的资源,并进行预加载。

同时,由于下面两点:

  1. 默认情况下,CSS
    被视为阻塞渲染的资源,这意味着浏览器将不会渲染任何已处理的内容,直至
    CSSOM 构建完毕。
  2. JavaScript 不仅可以读取和修改 DOM 属性,还可以读取和修改 CSSOM
    属性。

存在阻塞的 CSS 资源时,浏览器会延迟 JavaScript 的执行和 DOM 构建。另外:

  1. 当浏览器遇到一个 script 标记时,DOM 构建将暂停,直至脚本完成执行。
  2. JavaScript 可以查询和修改 DOM 与 CSSOM。
  3. CSSOM 构建时,JavaScript 执行将暂停,直至 CSSOM 就绪。

所以,script 标签的位置很重要。实际使用时,可以遵循下面两个原则:

  1. CSS 优先:引入顺序上,CSS 资源先于 JavaScript 资源。
  2. JavaScript 应尽量少影响 DOM 的构建。

浏览器的发展日益加快(目前的 Chrome 官方稳定版是
61),具体的渲染策略会不断进化,但了解这些原理后,就能想通它进化的逻辑。下面来看看
CSS 与 JavaScript 具体会怎样阻塞资源。

CSS 对象模型 (CSSOM)

在浏览器构建这个简单页面的 DOM 过程中,在文档的 head 中遇到了一个 link
标记,该标记引用一个外部 CSS
样式表:style.css。由于预见到需要利用该资源来渲染页面,它会立即发出对该资源的请求,并返回以下内容:

body { font-size: 16px }
p { font-weight: bold }
span { color: red }
p span { display: none }
img { float: right }

我们本可以直接在 HTML 标记内声明样式(内联),但让 CSS 独立于 HTML
有利于我们将内容和设计作为独立关注点进行处理:设计人员负责处理
CSS,开发者侧重于 HTML,等等。

与处理 HTML 时一样,我们需要将收到的 CSS
规则转换成某种浏览器能够理解和处理的东西。因此,我们会重复 HTML
过程,不过是为 CSS 而不是 HTML:

 必发88官网 12

CSS 字节转换成字符,接着转换成tokens和节点,最后链接到一个称为“CSS
对象模型”(CSSOM) 的树结构:

 必发88官网 13

CSSOM
为何具有树结构?为页面上的任何节点对象计算最后一组样式时,浏览器都会先从适用于该节点的最通用规则开始(例如,如果该节点是
body 元素的子元素,则应用所有 body
样式),然后通过应用更具体的规则以递归方式优化计算的样式。

以上面的 CSSOM 树为例进行更具体的阐述。任何置于 body
元素内span 标记中的文本都将具有 16 像素字号,并且颜色为红色
。font-size 指令从 body 向下级层叠至 span。不过,如果某个 span
标记是某个段落 (p) 标记的子项,则其内容将不会显示。

Also, note that the above tree is not the complete CSSOM tree and only
shows the styles we decided to override in our
stylesheet.每个浏览器都提供一组默认样式(也称为“User Agent
样式”),即我们的样式只是override这些默认样式。

要了解 CSS 处理所需的时间,可以在 DevTools
中记录时间线并寻找“Recalculate Style”事件:unlike DOM parsing, the
timeline doesn’t show a separate “Parse CSS” entry, and instead captures
parsing and CSSOM tree construction, plus the recursive calculation of
computed styles under this one event.

 必发88官网 14

我们的小样式表需要大约 0.6 毫秒的处理时间,影响页面上的 8 个元素 —
虽然不多,但同样会产生开销。不过,这 8 个元素从何而来呢?将 DOM 与 CSSOM
关联在一起的是渲染树必发88官网,。

回到未来 – 预解析(speculative parsing)

每当解析器遇到一个脚本就暂停意味着每个你加载的脚本都会推迟发现链接到
HTML 的其他资源。

如果你有几个类似的脚本和图片要加载,例如:

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

1
2
3
4
5
  <script src="slider.js"></script>
  <script src="animate.js"></script>
  <script src="cookie.js"></script>
  <img src="slide1.png">
  <img src="slide2.png">

这个过程过去是这样的:

必发88官网 15

这个状况在 2008 年左右改变了,当时 IE 引入了一个概念叫做 “先行下载”。
这是一种在同步的脚步执行的时候保持文件的下载的一种方法。Firefox,Chrome
和 Safari
随后效仿,如今大多数的浏览器都使用了这个技术,它们有着不同的名称。Chrome
和 Safari 称它为 “预扫描器” 而 Firefox 称它为预解析器。

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

上面例子的瀑布图现在看起来是这样的:

必发88官网 16

以这种方式触发的下载请求称之为 “预测”,因为很有可能脚本还是会改变 HTML
结构(还记得document.write吗?),导致了预测的浪费。虽然这是有可能的,但是却不常见,所以这就是为什么预解析仍然能够带来很大的性能提升。

而且其他浏览器只会对链接的资源进行这样的预加载。在 Firefox 中,HTML
解析器对 DOM
树的构建也是算法预测的。有利的一面是,当推测成功的时候,就没有必要重新解析文件的一部分了。缺点是,如果推测失败了,就需要更多的工作。

CSS

JavaScript

<style> p { color: red; }</style> <link rel=”stylesheet”
href=”index.css”>

1
2
<style> p { color: red; }</style>
<link rel="stylesheet" href="index.css">

这样的 link 标签(无论是否
inline)会被视为阻塞渲染的资源,浏览器会优先处理这些 CSS 资源,直至
CSSOM 构建完毕。

渲染树(Render-Tree)的关键渲染路径中,要求同时具有 DOM 和
CSSOM,之后才会构建渲染树。即,HTML 和 CSS 都是阻塞渲染的资源。HTML
显然是必需的,因为包括我们希望显示的文本在内的内容,都在 DOM
中存放,那么可以从 CSS 上想办法。

最容易想到的当然是精简 CSS
并尽快提供它
。除此之外,还可以用媒体类型(media
type)和媒体查询(media query)来解除对渲染的阻塞。

JavaScript

<link href=”index.css” rel=”stylesheet”> <link href=”print.css”
rel=”stylesheet” media=”print”> <link href=”other.css”
rel=”stylesheet” media=”(min-width: 30em) and (orientation:
landscape)”>

1
2
3
<link href="index.css" rel="stylesheet">
<link href="print.css" rel="stylesheet" media="print">
<link href="other.css" rel="stylesheet" media="(min-width: 30em) and (orientation: landscape)">

第一个资源会加载并阻塞。
第二个资源设置了媒体类型,会加载但不会阻塞,print
声明只在打印网页时使用。
第三个资源提供了媒体查询,会在符合条件时阻塞渲染。

渲染树构建、布局及绘制

CSSOM 树和 DOM
树合并成渲染树,然后用于计算每个可见元素的布局,并输出给绘制流程,将像素渲染到屏幕上。优化上述每一个步骤对实现最佳渲染性能至关重要。

浏览器根据 HTML 和 CSS 输入构建了 DOM 树和 CSSOM 树。
不过,它们是彼此完全独立的对象,分别capture文档不同方面的信息:一个描述内容,另一个则是描述需要对文档应用的样式规则。我们该如何将两者合并,让浏览器在屏幕上渲染像素呢?

  • DOM 树与 CSSOM
    树合并后形成渲染树,它只包含渲染网页所需的节点。遍历每个DOM树中的node节点,在CSSOM规则树中寻找当前节点的样式,生成渲染树。
  • 布局计算每个对象的精确位置和大小。
  • 最后一步是绘制,使用最终渲染树将像素渲染到屏幕上。

 必发88官网 17

第一步是让浏览器将 DOM 和 CSSOM
合并成一个“渲染树”,网罗网页上所有可见的 DOM
内容,以及每个节点的所有 CSSOM 样式信息。

 必发88官网 18

为构建渲染树,浏览器大体上完成了下列工作:

  1. 从 DOM 树的根节点开始遍历每个可见节点。
    • 某些节点不可见(例如脚本标记、元标记等),因为它们不会体现在渲染输出中,所以会被忽略。
    • 某些节点通过 CSS 隐藏,因此在渲染树中也会被忽略。例如 span
      节点上设置了“display: none”属性,所以也不会出现在渲染树中。
  2. 遍历每个可见节点,为其找到适配的 CSSOM
    规则并应用它们。从选择器的右边往左边开始匹配,也就是从CSSOM树的子节点开始往父节点匹配。
  3. Emit visible nodes with content and their computed styles.

注: visibility: hidden 与 display:
none 是不一样的。前者隐藏元素,但元素仍占据着布局空间(即将其渲染成一个空框),而后者
(display: none)
将元素从渲染树中完全移除,元素既不可见,也不是布局的组成部分。

最终输出的渲染同时包含了屏幕上的所有可见内容及其样式信息。有了渲染树,我们就可以进入“布局”阶段。

到目前为止,我们计算了哪些节点应该是可见的以及它们的计算样式,但我们尚未计算它们在设备视口内的确切位置和大小—这就是“布局”阶段,也称为“reflow”。

为弄清每个对象在网页上的确切大小和位置,浏览器从渲染树的根节点开始进行遍历。让我们考虑一个简单的实例:

<html>
  <head>
    <meta name="viewport" content="width=device-width,initial-scale=1">
    <title>Critial Path: Hello world!</title>
  </head>
  <body>
    <div style="width: 50%">
      <div style="width: 50%">Hello world!</div>
    </div>
  </body>
</html>

以上网页的正文包含两个嵌套 div:第一个(父)div
将节点的显示尺寸设置为视口宽度的 50%,父 div 包含的第二个
div宽度为其父项的 50%,即视口宽度的 25%。

 必发88官网 19

 

布局流程的输出是一个“盒模型”,它会精确地捕获每个元素在视口内的确切位置和尺寸:所有相对测量值都转换为屏幕上的绝对像素。

最后,既然我们知道了哪些节点可见、它们的computed
styles以及几何信息,我们终于可以将这些信息传递给最后一个阶段:将渲染树中的每个节点转换成屏幕上的实际像素。这一步通常称为”painting”
or “rasterizing.”。

Chrome DevTools
可以帮助我们对上述所有三个阶段的耗时进行深入的了解。让我们看一下最初“hello
world”示例的布局阶段:

 必发88官网 20

The “Layout” event captures the render tree construction, position, and
size calculation in the Timeline.

When layout is complete, the browser issues “Paint Setup” and “Paint”
events, which convert the render tree to pixels on the screen.

执行渲染树构建、布局和绘制所需的时间将取决于文档大小、应用的样式,以及运行文档的设备:文档越大,浏览器需要完成的工作就越多;样式越复杂,绘制需要的时间就越长(例如,单色的绘制开销“较小”,而阴影的计算和渲染开销则要“大得多”)。

下面简要概述了浏览器完成的步骤:

  1. 处理 HTML 标记并构建 DOM 树。
  2. 处理 CSS 标记并构建 CSSOM 树。
  3. 将 DOM 与 CSSOM 合并成一个渲染树。
  4. 根据渲染树来布局,以计算每个节点的几何信息。
  5. 将各个节点绘制到屏幕上。

如果 DOM 或 CSSOM
被修改,需要再执行一遍以上所有步骤,以确定哪些像素需要在屏幕上进行重新渲染。

Optimizing the critical rendering path is the process of minimizing
the total amount of time spent performing steps 1 through 5 in the above
sequence.
Doing so renders content to the screen as quickly as
possible and also reduces the amount of time between screen updates
after the initial render; that is, achieve higher refresh rates for
interactive content.

关于(预)加载

这种资源加载的方式带来了显著地性能提升,你不需要做任何事情就可以使用这种优势。然而,作为一个
web 开发者,了解预解析是如何工作的能帮你最大程度地利用它。

可以预加载的东西在浏览器之间有所不同,但所有的主要的浏览器都会预加载:

  • 脚本
  • 外部 CSS
  • 来自 img 标签的图片

Firefox 也会预加载 video 元素的 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);

1
2
3
4
var script = document.createElement(‘script’);
script.src = "//somehost.com/widget.js";
document.getElementsByTagName(‘head’)[0].appendChild(script);
 

你应该尽可能使浏览器能更轻松访问到重要的资源。你可以把他们放到 HTML
标签当中或者将要加载的脚本内联到文档的前面。然而,有时候需要一些不重要的资源晚一点被加载。这种情况,你通过
JavaScript 来加载他们来避免预解析。

你也可以看看这个 MDN
指南,里面讲述了如何针对预解析优化你的页面。

JavaScript

JavaScript 的情况比 CSS 要更复杂一些。观察下面的代码:

JavaScript

<p>Do not go gentle into that good night,</p>
<script>console.log(“inline”)</script> <p>Old age
should burn and rave at close of day;</p> <script
src=”app.js”></script> <p>Rage, rage against the dying of
the light.</p> <p>Do not go gentle into that good
night,</p> <script src=”app.js”></script> <p>Old
age should burn and rave at close of day;</p>
<script>console.log(“inline”)</script> <p>Rage, rage
against the dying of the light.</p>

1
2
3
4
5
6
7
8
9
10
11
<p>Do not go gentle into that good night,</p>
<script>console.log("inline")</script>
<p>Old age should burn and rave at close of day;</p>
<script src="app.js"></script>
<p>Rage, rage against the dying of the light.</p>
 
<p>Do not go gentle into that good night,</p>
<script src="app.js"></script>
<p>Old age should burn and rave at close of day;</p>
<script>console.log("inline")</script>
<p>Rage, rage against the dying of the light.</p>

这样的 script 标签会阻塞 HTML 解析,无论是不是 inline-script。上面的 P
标签会从上到下解析,这个过程会被两段 JavaScript
分别打算一次(加载、执行)。

所以实际工程中,我们常常将资源放到文档底部。

阻塞渲染的 CSS

默认情况下,CSS
被视为阻塞渲染的资源(但不阻塞html的解析),这意味着浏览器将不会渲染任何已处理的内容,直至
CSSOM
构建完毕请务必精简CSS,尽快提供它,并利用媒体类型和查询来解除对渲染的阻塞,以缩短首屏的时间。

在渲染树构建中,要求同时具有
DOM 和 CSSOM 才能构建渲染树。这会给性能造成严重影响:HTML
CSS 都是阻塞渲染的资源。 HTML 显然是必需的,因为如果没有
DOM,就没有可渲染的内容,但 CSS 的必要性可能就不太明显。如果在 CSS
不阻塞渲染的情况下尝试渲染一个普通网页会怎样?

  • 默认情况下,CSS 被视为阻塞渲染的资源。
  • 我们可以通过媒体类型和媒体查询将一些 CSS 资源标记为不阻塞渲染。
  • 浏览器会下载所有 CSS 资源,无论阻塞还是不阻塞。

没有 CSS 的网页实际上无法使用。所以浏览器将阻塞渲染,直至 DOM 和 CSSOM
全都准备就绪。

CSS
是阻塞渲染的资源。需要将它尽早、尽快地下载到客户端,以便缩短首次渲染的时间。

如果有一些 CSS
样式只在特定条件下(例如显示网页或将网页投影到大型显示器上时)使用,又该如何?如果这些资源不阻塞渲染,该有多好。

可以通过 CSS“媒体类型”和“媒体查询”来解决这类情况:

<link href=”style.css” rel=”stylesheet”>
<link href=”print.css” rel=”stylesheet” media=”print”>
<link href=”other.css” rel=”stylesheet” media=”(min-width: 40em)”>

媒体查询由媒体类型以及零个或多个检查特定媒体特征状况的表达式组成。例如,第一个样式表声明未提供任何媒体类型或查询,因此它适用于所有情况。也就是说它始终会阻塞渲染。第二个样式表则不然,它只在打印内容时适用—或许您想重新安排布局、更改字体等等,因此在网页首次加载时,该样式表不需要阻塞渲染。最后一个样式表声明提供了由浏览器执行的“媒体查询”:符合条件时,样式表会生效,浏览器将阻塞渲染,直至样式表下载并处理完毕。

通过使用媒体查询,我们可以根据特定用例(比如显示或打印),也可以根据动态情况(比如屏幕方向变化、尺寸调整事件等)定制外观。声明样式表时,请密切注意媒体类型和查询,因为它们将严重影响关键渲染路径的性能。

让我们考虑下面这些实例:

<link href=”style.css”    rel=”stylesheet”>
<link href=”style.css”    rel=”stylesheet” media=”all”>
<link href=”portrait.css” rel=”stylesheet”
media=”orientation:portrait”>
<link href=”print.css”    rel=”stylesheet” media=”print”>

  • 第一个声明阻塞渲染,适用于所有情况。
  • 第二个声明同样阻塞渲染:“all”是默认类型,和第一个声明实际上是等效的。
  • 第三个声明具有动态媒体查询,将在网页加载时计算。根据网页加载时设备的方向,portrait.css
    可能阻塞渲染,也可能不阻塞渲染。
  • 最后一个声明只在打印网页时应用,因此网页在浏览器中加载时,不会阻塞渲染。

最后,“阻塞渲染”仅是指浏览器是否需要暂停网页的首次渲染,直至该资源准备就绪。无论媒寻是否命中,浏览器都会下载上述所有的CSS样式表,只不过不阻塞渲染的资源对当前媒体不生效罢了。

defer 和 async

不过,同步的脚本阻塞解析器仍旧是个问题。并不是所有的脚本对用户体验都是同等的重要,例如那些用于监测和分析的脚本。解决方法呢?就是去尽可能地异步加载这些不那么重要的脚本。

deferasync
属性
提供给开发者一个方式来告诉浏览器哪些脚本是需要异步加载的。

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

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

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

必发88官网 21

async 脚本在它们完成下载完成后的第一时间执行,它处在 window
load
事件之前。 这意味着有可能(并且很有可能)设置了 async
的脚本不会按照它们在 HTML 中出现的顺序执行。这也意味着他们可能会中断 DOM
的构建。

无论它们在何处被指定,设置async
的脚本的加载有着较低的优先级。他们通常在所有其他脚本加载之后才加载,而不阻塞
DOM 构建。然而,如果一个指定async
的脚本很快就完成了下载,那么它的执行会阻塞 DOM
构建以及所有在之后才完成下载的同步脚。

必发88官网 22

注: async 和 defer 属性只对外部脚本起作用,如果没有 src
属性它们会被忽略。

改变阻塞模式:defer 与 async

为什么要将 script 加载的 defer 与 async
方式放到后面呢?因为这两种方式是的出现,全是由于前面讲的那些阻塞条件的存在。换句话说,defer
与 async 方式可以改变之前的那些阻塞情形。

首先,注意 async 与 defer 属性对于 inline-script
都是无效的,所以下面这个示例中三个 script 标签的代码会从上到下依次执行。

JavaScript

<!– 按照从上到下的顺序输出 1 2 3 –> <script async>
console.log(“1”); </script> <script defer> console.log(“2”);
</script> <script> console.log(“3”); </script>

1
2
3
4
5
6
7
8
9
10
<!– 按照从上到下的顺序输出 1 2 3 –>
<script async>
  console.log("1");
</script>
<script defer>
  console.log("2");
</script>
<script>
  console.log("3");
</script>

故,下面两节讨论的内容都是针对设置了 src 属性的 script 标签。

使用 JavaScript 添加交互

JavaScript
允许我们修改网页的方方面面:内容、样式以及它如何响应用户交互。不过,JavaScript
也会阻止 DOM 构建和延缓网页渲染。为了实现最佳性能,可以让 JavaScript
异步执行,并去除关键渲染路径中任何不必要的 JavaScript。

  • JavaScript 可以查询和修改 DOM 与 CSSOM。
  • JavaScript的 执行会阻止 CSSOM的构建,所以和CSSOM的构建是互斥的。
  • JavaScript blocks DOM construction unless explicitly declared as
    async.

JavaScript
是一种运行在浏览器中的动态语言,它允许对网页行为的几乎每一个方面进行修改:可以通过在
DOM 树中添加和移除元素来修改内容;可以修改每个元素的 CSSOM
属性;可以处理用户输入等等。为进行说明,让我们用一个简单的内联脚本对之前的“Hello
World”示例进行扩展:

<html>
  <head>
    <meta name="viewport" content="width=device-width,initial-scale=1">
    <link href="style.css" rel="stylesheet">
    <title>Critical Path: Script</title>
    <style> body { font-size: 16px };p { font-weight: bold };
    span { color: red };p span { display: none };
    img { float: right }</style>
  </head>
  <body>
    <p>Hello web performance students!</p>
    <div><img src="awesome-photo.jpg"></div>
    <script>
      var span = document.getElementsByTagName('span')[0];
      span.textContent = 'interactive'; // change DOM text content
      span.style.display = 'inline';  // change CSSOM property
      // create a new element, style it, and append it to the DOM
      var loadTime = document.createElement('div');
      loadTime.textContent = 'You loaded this page on: ' + new Date();
      loadTime.style.color = 'blue';
      document.body.appendChild(loadTime);
    </script>
  </body>
</html>
  • JavaScript 允许我们进入 DOM 并获取对隐藏的 span 节点的引用 —
    该节点可能未出现在渲染树中,却仍然存在于 DOM
    内。然后,在获得引用后,就可以更改其文本,并将 display
    样式属性从“none”替换为“inline”。现在,页面显示“Hello interactive
    students!”。
  • JavaScript 还允许我们在 DOM
    中创建、样式化、追加和移除新元素。从技术上讲,整个页面可以是一个大的
    JavaScript
    文件,此文件逐一创建元素并对其进行样式化。但是在实践中,使用 HTML 和
    CSS 要简单得多。

尽管 JavaScript
为我们带来了许多功能,不过也在页面渲染方式和时间方面施加了更多限制。

preload

如果你想要延迟处理一些脚本,那么asyncdefer
非常棒。那网页上那些对用户体验至关重要的东西呢?预解析器很方便,但是它们只会预加载少数类型的资源并遵循其逻辑。通常的目的都是首先交付
CSS,因为它会阻塞渲染。同步的脚本总是比异步的脚本拥有更高的优先级。视口中可见的图像会比那些底下的图片先下载完。还有字体,视频,SVG…
总而言之 — 这个过程很复杂。

作为作者,你知道哪些资源对你的页面渲染来说是最重要的。它们其中一些经常深藏在
CSS
或者是脚本当中,甚至浏览器需要花上很长一段时间才会发现他们。对于那些重要的资源,你现在可以使用“
来告诉浏览器你需要尽快地加载它们。

你只需要写上:

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

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

你几乎可以链接到任何东西上,as
属性告诉浏览器要下载的是什么。一些可能的值是:

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

你可以在MDN上查看剩余的内容类型。

字体可能是隐藏在CSS中最重要的东西。它们对页面上文字的渲染非常地关键,但是它们知道浏览器确认它们会被使用之前都不会被加载。
这个检查只发生在 CSS 已经被解析,应用,并且浏览器已经将 CSS
规则匹配到对应的 DOM
节点上时。这个过程在页面加载的过程中发生的相当晚,并且常常导致文字渲染中不必要的延迟。你可以通过使用
preload 属性来避免。

有一点要注意,要预加载字体你还必须设置crossorigin
属性,即使字体在同一个域名下:

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

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

preload
特性目前只有有限的支持度,因为其他浏览器还在推出它的过程中。你可以在这里查看进度。

defer

JavaScript

<script src=”app1.js” defer></script> <script
src=”app2.js” defer></script> <script src=”app3.js”
defer></script>

1
2
3
<script src="app1.js" defer></script>
<script src="app2.js" defer></script>
<script src="app3.js" defer></script>

defer 属性表示延迟执行引入的 JavaScript,即这段 JavaScript 加载时 HTML
并未停止解析,这两个过程是并行的。整个 document 解析完毕且 defer-script
也加载完成之后(这两件事情的顺序无关),会执行所有由 defer-script 加载的
JavaScript 代码,然后触发 DOMContentLoaded 事件。

defer 不会改变 script 中代码的执行顺序,示例代码会按照 1、2、3
的顺序执行。所以,defer 与相比普通 script,有两点区别:载入 JavaScript
文件时不阻塞 HTML 的解析,执行阶段被放到 HTML 标签解析完成之后。

首先,请注意上例中的内联脚本靠近网页底部。为什么呢?如果我们将脚本移至 span元素前面,就会脚本运行失败,并提示在文档中找不到对任何span 元素的引用

即 getElementsByTagName(‘span’) 会返回 null。这透露出一个重要事实:脚本在文档的何处插入,就在何处执行。当
HTML 解析器遇到一个 script 标记时,它会暂停构建
DOM,将控制权移交给 JavaScript 引擎;等 JavaScript
引擎运行完毕,浏览器会从中断的地方恢复 DOM 构建。

换言之,我们的脚本块在运行时找不到网页中任何靠后的元素,因为它们尚未被处理!或者说:执行内联脚本会阻止
DOM 构建,也就延缓了首次渲染。

在网页中引入脚本的另一个微妙事实是,它们不仅可以读取和修改 DOM
属性,还可以读取和修改 CSSOM 属性。实际上,示例中就是这么做的:将 span
元素的 display 属性从 none 更改为
inline。最终结果如何?我们现在遇到了race condition(资源竞争)。

如果浏览器尚未完成 CSSOM
的下载和构建,而却想在此时运行脚本,会怎样?答案很简单,对性能不利:浏览器将延迟脚本执行和
DOM 构建,直至其完成 CSSOM 的下载和构建。

简言之,JavaScript 在 DOM、CSSOM 和 JavaScript
执行之间引入了大量新的依赖关系,从而可能导致浏览器在处理以及在屏幕上渲染网页时出现大幅延迟:

  • 脚本在文档中的位置很重要。
  • 当浏览器遇到一个 script 标记时,DOM 构建将暂停,直至脚本完成执行。
  • JavaScript 可以查询和修改 DOM 与 CSSOM。
  • JavaScript 执行将暂停,直至 CSSOM 就绪。即CSSDOM构建的优先级更高。

“优化关键渲染路径”在很大程度上是指了解和优化 HTML、CSS 和 JavaScript
之间的依赖关系谱。

结论

浏览器是自 90
年代以来一直在进化的极其复杂的野兽。我们已经讨论了一些遗留问题以及 Web
开发中的一些最新标准。根据这些指南书写你的代码能够帮助你选择最好的策略来提供更加流畅的浏览器体验。

如果你想了解更多关于浏览器的工作原理,你可以查看其他的文章:

走进 Quantum :
什么是浏览器引擎?

深入了解一个超级快的 CSS 引擎: Quantum CSS (也称作
Stylo)

1 赞 1 收藏
评论

必发88官网 23

async

JavaScript

<script src=”app.js” async></script> <script src=”ad.js”
async></script> <script src=”statistics.js”
async></script>

1
2
3
<script src="app.js" async></script>
<script src="ad.js" async></script>
<script src="statistics.js" async></script>

async 属性表示异步执行引入的 JavaScript,与 defer
的区别在于,如果已经加载好,就会开始执行——无论此刻是 HTML 解析阶段还是
DOMContentLoaded 触发之后。需要注意的是,这种方式加载的 JavaScript
依然会阻塞 load 事件。换句话说,async-script 可能在 DOMContentLoaded
触发之前或之后执行,但一定在 load 触发之前执行。

从上一段也能推出,多个 async-script
的执行顺序是不确定的。值得注意的是,向 document 动态添加 script
标签时,async 属性默认是 true,下一节会继续这个话题。

解析器阻塞与异步 JavaScript

默认情况下,JavaScript
执行会“阻塞解析器”:当浏览器遇到文档中的脚本时,它必须暂停 DOM
构建,将控制权移交给 JavaScript 运行时,让脚本执行完毕,然后再继续构建
DOM。实际上,内联脚本始终会阻止解析器,除非编写额外代码来推迟它们的执行。

通过 script 标签引入的脚本又怎样:

<html>
  <head>
    <meta name="viewport" content="width=device-width,initial-scale=1">
    <link href="style.css" rel="stylesheet">
    <title>Critical Path: Script External</title>
  </head>
  <body>
    <p>Hello web performance students!</p>
    <div><img src="awesome-photo.jpg"></div>
    <script src="app.js"></script>
  </body>
</html>

app.js

var span = document.getElementsByTagName('span')[0];
span.textContent = 'interactive'; // change DOM text content
span.style.display = 'inline';  // change CSSOM property
// create a new element, style it, and append it to the DOM
var loadTime = document.createElement('div');
loadTime.textContent = 'You loaded this page on: ' + new Date();
loadTime.style.color = 'blue';
document.body.appendChild(loadTime);

无论我们使用 <script> 标记还是内联 JavaScript
代码段,两者能够以相同方式工作。
在两种情况下,浏览器都会先暂停并执行脚本,然后才会处理剩余文档。如果是外部
JavaScript
文件,浏览器必须停下来,等待从磁盘、缓存或远程服务器获取脚本,这就可能给关键渲染路径增加更长的延迟。

默认情况下,所有 JavaScript
都会阻止解析器。由于浏览器不了解脚本计划在页面上执行什么操作,它会作最坏的假设并阻止解析器。向浏览器传递脚本不需要在引用位置执行的信号既可以让浏览器继续构建
DOM,也能够让脚本在就绪后执行。为此,我们可以将脚本标记为异步:

<html>
  <head>
    <meta name="viewport" content="width=device-width,initial-scale=1">
    <link href="style.css" rel="stylesheet">
    <title>Critical Path: Script Async</title>
  </head>
  <body>
    <p>Hello web performance students!</p>
    <div><img src="awesome-photo.jpg"></div>
    <script src="app.js" async></script>
  </body>
</html>

向 script
标记添加异步关键字可以指示浏览器在等待脚本可用期间(仅指下载期间,因为所有脚本的执行都会阻塞解析器)不阻止
DOM 构建,这样可以显著提升性能。

document.createElement

使用 document.createElement 创建的 script 默认是异步的,示例如下。

JavaScript

console.log(document.createElement(“script”).async); // true

1
console.log(document.createElement("script").async); // true

所以,通过动态添加 script 标签引入 JavaScript
文件默认是不会阻塞页面的。如果想同步执行,需要将 async 属性人为设置为
false。

如果使用 document.createElement 创建 link 标签会怎样呢?

JavaScript

const style = document.createElement(“link”); style.rel = “stylesheet”;
style.href = “index.css”; document.head.appendChild(style); // 阻塞?

1
2
3
4
const style = document.createElement("link");
style.rel = "stylesheet";
style.href = "index.css";
document.head.appendChild(style); // 阻塞?

其实这只能通过试验确定,已知的是,Chrome
中已经不会阻塞渲染,Firefox、IE
在以前是阻塞的,现在会怎样我没有试验。

分析关键渲染路径性能

发现和解决关键渲染路径性能瓶颈需要充分了解常见的陷阱。让我们踏上实践之旅,找出常见的性能模式,从而帮助您优化网页。

优化关键渲染路径能够让浏览器尽可能快地绘制网页:更快的网页渲染速度可以提高吸引力、增加网页浏览量以及提高转化率。为了最大程度减少访客看到空白屏幕的时间,我们需要优化加载的资源及其加载顺序。

为帮助说明这一流程,让我们先从可能的最简单情况入手,逐步构建我们的网页,使其包含更多资源、样式和应用逻辑。在此过程中,我们还会对每一种情况进行优化,以及了解可能出错的环节。

到目前为止,我们只关注了资源(CSS、JS 或 HTML
文件)可供处理后浏览器中会发生的情况,而忽略了从缓存或从网络获取资源所需的时间。我们作以下假设:

  • 到服务器的网络往返(传播延迟时间)需要 100 毫秒。
  • HTML 文档的服务器响应时间为 100
    毫秒,所有其他文件的服务器响应时间均为 10 毫秒。

Hello World 体验

<html>
  <head>
    <meta name="viewport" content="width=device-width,initial-scale=1">
    <title>Critical Path: No Style</title>
  </head>
  <body>
    <p>Hello web performance students!</p>
    <div><img src="awesome-photo.jpg"></div>
  </body>
</html>

我们将从基本 HTML 标记和单个图像(无 CSS 或 JavaScript)开始。让我们在
Chrome DevTools 中打开 Network 时间线并检查生成的资源瀑布:

 必发88官网 24

正如预期的一样,HTML 文件下载花费了大约 200
毫秒。请注意,蓝线的透明部分表示浏览器在网络上等待(即尚未收到任何响应字节)的时间,而不透明部分表示的是收到第一批响应字节后完成下载的时间。HTML
下载量很小 (<4K),我们只需单次往返便可获取整个文件。因此,获取 HTML
文档大约需要 200
毫秒,其中一半的时间花费在网络等待上,另一半花费在等待服务器响应上。

当 HTML 内容可用后,浏览器会解析字节,将它们转换成tokens,然后构建 DOM
树。请注意,为方便起见,DevTools 会在底部记录 DOMContentLoaded
事件的时间(216 毫秒),该时间同样与蓝色垂直线相符。HTML
下载结束与蓝色垂直线 (DOMContentLoaded)
之间的间隔是浏览器构建 DOM 树所花费的时间
在本例中仅为几毫秒。

请注意,我们的“趣照”并未阻止 domContentLoaded 事件。这证明,我们构建渲染树甚至绘制网页时无需等待页面上的每个静态资源:并非所有资源都对快速提供首次绘制具有关键作用。事实上,当我们谈论关键渲染路径时,通常谈论的是
HTML 标记、CSS 和
JavaScript。图像不会阻止页面的首次渲染,不过,我们当然也应该尽力确保系统尽快绘制图像!

That said, the load event (also known as onload), is blocked on the
image: DevTools reports the onload event at 335ms. Recall that the
onload event marks the point at which all resources that the page
requires have been downloaded and processed; at this point (the red
vertical line in the waterfall), the loading spinner can stop spinning
in the browser.

document.write 与 innerHTML

通过 document.write 添加的 link 或 script 标签都相当于添加在 document
中的标签,因为它操作的是 document stream(所以对于 loaded 状态的页面使用
document.write 会自动调用
document.open,这会覆盖原有文档内容)。即正常情况下, link
会阻塞渲染,script 会同步执行。不过这是不推荐的方式,Chrome
已经会显示警告,提示未来有可能禁止这样引入。如果给这种方式引入的 script
添加 async 属性,Chrome 会检查是否同源,对于非同源的 async-script
是不允许这么引入的。

如果使用 innerHTML 引入 script 标签,其中的 JavaScript
不会执行。当然,可以通过 eval() 来手工处理,不过不推荐。如果引入 link
标签,我试验过在 Chrome
中是可以起作用的。另外,outerHTML、insertAdjacentHTML()
应该也是相同的行为,我并没有试验。这三者应该用于文本的操作,即只使用它们添加
text 或普通 HTML Element。

结合使用 JavaScript 和 CSS

“Hello World
experience”页面虽然看起来简单,但背后却需要做很多工作。在实践中,我们还需要
HTML 之外的其他资源:我们可能需要 CSS
样式表以及一个或多个用于为网页增加一定交互性的脚本。让我们将两者结合使用,看看效果如何:

<html>
  <head>
    <title>Critical Path: Measure Script</title>
    <meta name="viewport" content="width=device-width,initial-scale=1">
    <link href="style.css" rel="stylesheet">
  </head>
  <body onload="measureCRP()">
    <p>Hello web performance students!</p>
    <div><img src="awesome-photo.jpg"></div>
    <script src="timing.js"></script>
  </body>
</html>

添加 JavaScript 和 CSS 之前:

 必发88官网 25

 

添加 JavaScript 和 CSS 之后:

 必发88官网 26

添加外部 CSS 和 JavaScript
文件将额外增加两个瀑布请求,浏览器差不多会同时发出这两个请求。不过,请注意,现在 domContentLoaded 事件与 onload 事件之间的时间差小多了。这是怎么回事?

  • 与纯 HTML 示例不同,我们还需要获取并解析 CSS 文件才能构建
    CSSOM,要想构建渲染树,DOM 和 CSSOM 缺一不可。
  • 由于网页上还有一个阻塞解析器的JavaScript 文件,系统会在下载并解析
    CSS 文件之前阻止 domContentLoaded事件:因为 JavaScript 可能会查询
    CSSOM,必须在下载 CSS 文件之后才能执行 JavaScript。

如果我们用内联脚本替换外部脚本会怎样?即使直接将脚本内联到网页中,浏览器仍然无法在构建
CSSOM 之前执行脚本。简言之,内联 JavaScript 也会阻止解析器。

不过,尽管内联脚本会阻止
CSS,但这样做是否能加快页面渲染速度呢?让我们尝试一下,看看会发生什么。

外部 JavaScript:

 必发88官网 27

内联 JavaScript:

 必发88官网 28

我们减少了一个请求,但 onload 和 domContentLoaded 时间实际上没有变化。为什么呢?怎么说呢,我们知道,这与
JavaScript 是内联的还是外部的并无关系,因为只要浏览器遇到 script
标记,就会进行阻止,并等到之前的css文件的 CSSOM
构建完毕。此外,在我们的第一个示例中,浏览器是并行下载 CSS 和
JavaScript,并且差不多是同时完成。在此实例中,内联 JavaScript
代码并无多大意义。但是,我们可以通过多种策略加快网页的渲染速度。

首先回想一下,所有内联脚本都会阻止解析器,但对于外部脚本,可以添加“async”关键字来解除对解析器的阻止。让我们撤消内联,尝试一下这种方法:

<html>
  <head>
    <title>Critical Path: Measure Async</title>
    <meta name="viewport" content="width=device-width,initial-scale=1">
    <link href="style.css" rel="stylesheet">
  </head>
  <body onload="measureCRP()">
    <p>Hello web performance students!</p>
    <div><img src="awesome-photo.jpg"></div>
    <script async src="timing.js"></script>
  </body>
</html>

阻止解析器的(外部)JavaScript:

 必发88官网 29

异步(外部)JavaScript:

 必发88官网 30

效果好多了!解析 HTML
之后不久即会触发 domContentLoaded 事件;浏览器已得知不要阻止
JavaScript,并且由于没有其他阻止解析器的脚本,CSSOM 构建也可并行进行了。

或者,我们也可以同时内联 CSS 和 JavaScript:

<html>
  <head>
    <title>Critical Path: Measure Inlined</title>
    <meta name="viewport" content="width=device-width,initial-scale=1">
    <style>
      p { font-weight: bold }
      span { color: red }
      p span { display: none }
      img { float: right }
    </style>
  </head>
  <body>
    <p>Hello web performance students!</p>
    <div><img src="awesome-photo.jpg"></div>
    <script>
      var span = document.getElementsByTagName('span')[0];
      span.textContent = 'interactive'; // change DOM text content
      span.style.display = 'inline';  // change CSSOM property
      // create a new element, style it, and append it to the DOM
      var loadTime = document.createElement('div');
      loadTime.textContent = 'You loaded this page on: ' + new Date();
      loadTime.style.color = 'blue';
      document.body.appendChild(loadTime);
    </script>
  </body>
</html>

必发88官网 31

请注意,domContentLoaded 时间与前一示例中的时间实际上相同;只不过没有将
JavaScript 标记为异步,而是同时将 CSS 和 JS 内联到网页本身。这会使 HTML
页面显著增大,但好处是浏览器无需等待获取任何外部资源,网页已经内置了所有资源。

即便是非常简单的网页,优化关键渲染路径也并非轻而易举:需要了解不同资源之间的依赖关系图,需要确定哪些资源是“关键资源”,还必须在不同策略中做出选择,找到在网页上加入这些资源的恰当方式。这一问题不是一个解决方案能够解决的,每个页面都不尽相同。您需要遵循相似的流程,自行找到最佳策略。

不过,我们可以回过头来,看看能否找出某些常规性能模式。

性能模式

最简单的网页只包括 HTML 标记;没有 CSS,没有
JavaScript,也没有其他类型的资源。要渲染此类网页,浏览器需要发起请求,等待
HTML 文档到达,对其进行解析,构建 DOM,最后将其渲染在屏幕上:

<html>
  <head>
    <meta name="viewport" content="width=device-width,initial-scale=1">
    <title>Critical Path: No Style</title>
  </head>
  <body>
    <p>Hello web performance students!</p>
    <div><img src="awesome-photo.jpg"></div>
  </body>
</html>

必发88官网 32

T0 与
T1 之间的时间捕获的是网络和服务器处理时间。在最理想的情况下(如果
HTML 文件较小),我们只需一次网络往返便可获取整个文档。由于 TCP
传输协议工作方式的缘故,较大文件可能需要更多次的往返。因此,在最理想的情况下,上述网页具有单次往返(最少)关键渲染路径。

现在,我们还以同一网页为例,但这次使用外部 CSS 文件:

<html>
  <head>
    <meta name="viewport" content="width=device-width,initial-scale=1">
    <link href="style.css" rel="stylesheet">
  </head>
  <body>
    <p>Hello web performance students!</p>
    <div><img src="awesome-photo.jpg"></div>
  </body>
</html>

 必发88官网 33

我们同样需要一次网络往返来获取 HTML 文档,然后检索到的标记告诉我们还需要
CSS 文件;这意味着,浏览器需要返回服务器并获取
CSS,然后才能在屏幕上渲染网页。因此,这个页面至少需要两次往返才能显示出来。CSS
文件同样可能需要多次往返,因此重点在于“最少”。

让我们定义一下用来描述关键渲染路径的词汇:

  • 关键资源: 可能阻止网页首次渲染的资源。
  • 关键路径长度: 获取所有关键资源所需的往返次数或总时间。
  • 关键字节: 实现网页首次渲染所需的总字节数,它是所有关键资源传送文件大小的总和。我们包含单个
    HTML 页面的第一个示例包含一项关键资源(HTML 文档);关键路径长度也与
    1 次网络往返相等(假设文件较小),而总关键字节数正好是 HTML
    文档本身的传送大小。

现在,让我们将其与上面 HTML + CSS 示例的关键路径特性对比一下:

必发88官网 34

  • 2 项关键资源
  • 2 次或更多次往返的最短关键路径长度
  • 9 KB 的关键字节

我们同时需要 HTML 和 CSS 来构建渲染树。所以,HTML 和 CSS
都是关键资源:CSS 仅在浏览器获取 HTML
文档后才会获取,因此关键路径长度至少为两次往返。两项资源相加共计 9KB
的关键字节。

现在,让我们向组合内额外添加一个 JavaScript 文件。

<html>
  <head>
    <meta name="viewport" content="width=device-width,initial-scale=1">
    <link href="style.css" rel="stylesheet">
  </head>
  <body>
    <p>Hello web performance students!</p>
    <div><img src="awesome-photo.jpg"></div>
    <script src="app.js"></script>
  </body>
</html>

我们添加了 app.js,它既是网页上的外部 JavaScript
静态资源,又是一种解析器阻止(即关键)资源。更糟糕的是,为了执行
JavaScript 文件,我们还需要进行阻塞并等待 CSSOM;因为JavaScript 可以查询
CSSOM,因此在下载 style.css 并构建 CSSOM 之前,浏览器将会暂停解析。

 必发88官网 35

即便如此,如果我们实际查看一下该网页的“网络瀑布”,就会注意到 CSS 和
JavaScript 请求差不多是同时发起的;浏览器获取
HTML,发现两项资源并发起两个请求。因此,上述网页具有以下关键路径特性:

  • 3 项关键资源
  • 2 次或更多次往返的最短关键路径长度
  • 11 KB 的关键字节

现在,我们拥有了三项关键资源,关键字节总计达 11
KB,但我们的关键路径长度仍是两次往返,因为我们可以同时传送 CSS 和
JavaScript。了解关键渲染路径的特性意味着能够确定哪些是关键资源,此外还能了解浏览器如何安排资源的获取时间。让我们继续探讨示例。

在与网站开发者交流后,我们意识到我们在网页上加入的 JavaScript
不必具有阻塞作用:网页中的一些分析代码和其他代码不需要阻止网页的渲染。了解了这一点,我们就可以向
script 标记添加“async”属性来解除对解析器的阻止:

<html>
  <head>
    <meta name="viewport" content="width=device-width,initial-scale=1">
    <link href="style.css" rel="stylesheet">
  </head>
  <body>
    <p>Hello web performance students!</p>
    <div><img src="awesome-photo.jpg"></div>
    <script src="app.js" async></script>
  </body>
</html>

必发88官网 36

 

 异步脚本具有以下几个优点:

  • 脚本不再阻止解析器,也不再是关键渲染路径的组成部分。
  • 由于没有其他关键脚本,CSS 也不需要阻止 domContentLoaded 事件。
  • domContentLoaded 事件触发得越早,其他应用逻辑开始执行的时间就越早。

因此,我们优化过的网页现在恢复到了具有两项关键资源(HTML 和
CSS),最短关键路径长度为两次往返,总关键字节数为 9 KB。

最后,如果 CSS 样式表只需用于打印,那会如何呢?

<html>
  <head>
    <meta name="viewport" content="width=device-width,initial-scale=1">
    <link href="style.css" rel="stylesheet" media="print">
  </head>
  <body>
    <p>Hello web performance students!</p>
    <div><img src="awesome-photo.jpg"></div>
    <script src="app.js" async></script>
  </body>
</html>

 必发88官网 37

因为 style.css 资源只用于打印,浏览器不必阻止它便可渲染网页。所以,只要
DOM
构建完毕,浏览器便具有了渲染网页所需的足够信息。因此,该网页只有一项关键资源(HTML
文档),并且最短关键渲染路径长度为一次往返。

参考资料

Mobile Analysis in PageSpeed
Insights

Web
Fundamentals

MDN – HTML element
reference

1 赞 4 收藏 2
评论

必发88官网 23

总结:

By default,CSS is treated as a render blocking resource, which means
that the browser won’t render any processed content until the CSSOM is
constructed.
html和css都是阻塞渲染的资源,所以要尽快构建完DOM和CSSDOM才能最快显示首屏。但是CSS解析和HTML解析可以并行。 

当 HTML 解析器遇到一个 script 标记时,它会暂停构建
DOM,下载js文件(来源于外部/内联/缓存),然后将控制权移交给 JavaScript
引擎(此时若在脚本引用其后的元素,会发生引用错误);等 JavaScript
引擎运行完毕,浏览器会从中断的地方恢复 DOM
构建。也就是如果页面有script标签,DOMContentLoaded事件需要等待JS执行完才触发。但是可以将脚本标记为异步,在下载js文件的过程中不会阻塞DOM的构建。

defer 和 async都是异步下载js文件,但也有区别:
defer属性只有ie支持,该属性的脚本都是在页面解析完毕之后执行,而且延迟脚本不一定按照先后顺序执行。
async的js在下载完后会立即执行(因此脚本所执行的顺序并不是脚本在代码中的顺序,有可能后面出现的脚本先加载成功先执行)。

异步资源不会阻塞解析器,让浏览器避免在执行脚本之前受阻于
CSSOM的构建。通常,如果脚本可以使用 async
属性,意味着它并非首次渲染所必需,可以考虑在首次渲染后异步加载脚本。

Race Condition

What if the browser hasn’t finished downloading and building the CSSOM
when we want to run our script? The answer is simple and not very good
for performance: the browser delays script execution and DOM
construction until it has finished downloading and constructing the
CSSOM.即script标签中的JS需要等待位于其前面的CSS加载完才执行。

HTML解析器怎么构建DOM树的?DOM树和html标签是一一对应的,在从上往下解析html时,会边解析边构建DOM。如果碰到外部资源(link或script)时,会进行外部资源的加载。外部资源是js时会暂停html解析,等js加载和执行完才继续;外部资源是css时不影响html解析,但影响首屏渲染。

domContentLoaded:当初始 HTML
文档已经完成加载和解析成DOM树时触发,不会等CSS文件、图片、iframe加载完成。
load:when all resources(including images,) that the page requires
have been downloaded and processed.通过动态获取的资源和load事件无关。 

相关文章