浏览器页面渲染机制

发送 & 接收信息

数据是以“数据包”的形式通过互联网发送,而数据包以字节为单位。当你编写一些 HTML、CSS 和 JS,并试图在浏览器中打开 HTML 文件时,浏览器会从你的硬盘(或网络)中读取 HTML 的原始字节。明白了吗?浏览器读取的是原始数据字节,而不是你编写的代码的实际字符。

让我们继续。浏览器接收字节数据,但是,它用这些数据什么都做不了。数据的原始字节必须转换为它所理解的形式,这是第一步。

从 HTML 的原始字节到 DOM

浏览器对象需要处理的是文档对象模型(DOM)对象。那么,DOM 对象是从何而来的呢?这很简单。首先,将原始数据字节转换为字符。这一点,你可以通过你所编写的代码的字符看到。这种转换是基于 HTML 文件的字符编码完成的。至此,浏览器已经从原始数据字节转换为文件中的实际字符。但这不是最终的结果。这些字符会被进一步解析为一些称为“标记(token)”的东西

那么,这些标记是什么?文本文件中的一堆字符对浏览器引擎而言没什么用处。如果没有这个标记化过程,那么这一堆堆字符只会生成一系列毫无意义的文本,即 HTML 代码——不会生成一个真正的网站。

当你保存一个扩展名为.html 的文件时,就向浏览器引擎发出了把文件解析为 HTML 文档的信号。浏览器“解释”这个文件的方式是首先解析它。在解析过程中,特别是在标记化过程中,浏览器会解析 HTML 文件中的每个开始和结束“标签(tag)”。解析器可以识别尖括号中的每个字符串,如“< html>”、“< p>”,也可以推断出适用于其中任何一个字符串的规则集。例如,表示锚标签的标记与表示段落标签的标记具有不同的属性。

从概念上讲,你可以将标记看作某种数据结构,它包含关于某个 HTML 标签的信息。本质上,HTML 文件会被分解成称为标记的小的解析单元。浏览器就是这样开始识别你所编写的内容的。

但标记还不是最终的结果。标记化完成后,接下来,标记将被转换为节点。你可以将节点看作是具有特定属性的不同对象。实际上,更好的解释是,将节点看作是文档对象树中的独立实体。但节点仍然不是最终结果。

现在,让我们看一下最后一点。在创建好之后,这些节点将被链接到称为 DOM 的树数据结构中。DOM 建立起了父子关系、相邻兄弟关系等。在这个 DOM 对象中,每个节点之间都建立了关系。现在,这是我们可以使用的东西了。

Bytes=> characters=>Tokens=>Node=>Dom

字节=>字符=>标记=>节点=>Dom树

根据 HTML 文件的大小,DOM 的构建过程可能需要一些时间。无论文件多小,它都需要一些时间。

CSS 如何转换?

DOM 已经创建。带有一些 CSS 的典型 HTML 文件会包含下面这样的样式表链接:

<!DOCTYPE html>
<html>
<head>
    <link rel="stylesheet" type="text/css" media="screen" href="main.css" />
</head>
<body>

</body>
</html>

当浏览器接收到原始数据字节并启动 DOM 构建过程时,它还会发出请求来获取链接的 main.css 样式表。当浏览器开始解析 HTML 时,在找到 css 文件的链接标签的同时,它会发出请求来获取它。可能你已经猜到,浏览器还是接收 CSS 数据的原始字节,从互联网或是本地磁盘。

但是,浏览器如何处理这些 CSS 数据的原始字节呢?

从 CSS 的原始字节到 CSSOM

当浏览器接收到 CSS 的原始字节时,会启动一个和处理 HTML 原始字节类似的过程。就是说,原始数据字节被转换成字符,然后标记,然后形成节点,最后形成树结构。

什么是树结构?大多数人都知道 DOM 这个词。同样,也有一种 CSS 树结构,称为 CSS 对象模型,简称为 CSSOM

你知道,浏览器不能使用 HTML 或 CSS 的原始字节。必须将其转换成它能识别的形式,也就是这些树形结构

CSS 有一个叫做级联的东西。级联是浏览器确定如何在元素上应用样式的机制。

由于影响元素的样式可能来自父元素,即通过继承,或者已经在元素本身设置,所以 CSSOM 树结构变得很重要。为什么?这是因为浏览器必须递归遍历 CSS 树结构并确定影响特定元素的样式

一切顺利。浏览器有了 DOM 和 CSSOM 对象。现在,我们能在屏幕上呈现一些东西了吗?

渲染树

我们现在得到的是两个独立的树结构,它们似乎没有共同的目标。

DOM 和 CSSOM 树结构是两个独立的树结构。DOM 中包含关于页面 HTML 元素关系的所有信息,而 CSSOM 则包含关于元素样式的信息。好了,浏览器现在把 DOM 和 CSSOM 树组合成一棵渲染树。

DOM + CSSOM = 渲染树

渲染树包含页面上所有关于可见 DOM 内容的信息以及不同节点所需的所有 CSSOM 信息。注意,如果一个元素被 CSS 隐藏,例如使用 display; none,那么节点就不会包含在渲染树中。隐藏元素会出现在 DOM 中,但不会出现在渲染树中。这是因为渲染树结合了来自 DOM 和 CSSOM 的信息,所以它知道不能把隐藏元素包含在树中。

构建好渲染树之后,浏览器将继续下个步骤:布局!

布局

现在,我们有了屏幕上的内容和所有可见内容的样式信息——但是我们并没有实际在屏幕上渲染任何内容。首先,浏览器必须计算页面上每个对象的确切大小和位置。这就好比是,把要在页面上渲染的所有元素的内容和样式信息传递给一个有才华的数学家。然后,这位数学家用浏览器的视窗计算出每个元素的确切位置和大小。

这个布局步骤考虑了从 DOM 和 CSSOM 接收到的内容和样式,并执行了所有必要的布局计算。有时,你会听到人们把这个“布局”阶段称为“回流(reflow)

艺术家出场

现在,每个元素的确切位置已经计算出来,剩下的就是将元素“绘制”到屏幕上。

考虑一下。我们已经得到了在屏幕上显示元素所需的所有信息。我们只要把它展示给用户。这就是这个阶段的全部工作。有了元素内容(DOM)样式(CSSOM)计算得出的元素的精确布局信息,浏览器现在就可以将节点逐个“绘制”到屏幕上了。元素现在终于呈现在屏幕上了!

渲染阻塞资源

当你听到“渲染阻塞(render-blocking)”时,你会想到什么?我猜你想的是,“有东西阻止了屏幕上节点的实际绘制”。如果你这么说,那你说的完全正确!

优化网站的第一准则是让最重要的 HTML 和 CSS 尽可能快地传递到客户端。在成功绘制之前,必须构造 DOM 和 CSSOM,因此,HTML 和 CSS 都是渲染阻塞资源。关键是,你应该尽快将 HTML 和 CSS 提供给客户端,以优化应用程序的首次渲染时间。

JavaScript 如何执行?

一个好的 Web 应用程序肯定会使用一些 JavaScript。这是一定的。JavaScript 的“问题”在于你可以使用 JavaScript 修改页面的内容和样式。通过这种方式,你可以从 DOM 树中删除元素和添加元素,还可以通过 JavaScript 修改元素的 CSSOM 属性

<!DOCTYPE html>
<html>

<head>
    <meta name="viewport" content="width=device-width,initial-scale=1">
    <title>Medium Article Demo</title>
    <link rel="stylesheet" href="style.css">
</head>

<body>
    <p id="header">How Browser Rendering Works</p>
    <div><img src="https://i.imgur.com/jDq3k3r.jpg"></div>
</body>

</html>

这是一个非常简单的文档。样式表 style.css 只有下面一个声明语句:

body {
  background: #8cacea;
}

根据前面的解释,浏览器从磁盘(或网络)读取 HTML 文件的原始字节并将其转换为字符。字符被进一步解析为标记。当解析器遇到< link rel="stylesheet" href="style.css">时,就会请求获取 CSS 文件 style.css。DOM 构造继续进行,当 CSS 文件返回一些内容后,CSSOM 构造就开始了

引入 JavaScript 后,这个过程会发生什么变化?要记住,其中最重要的一件事情是,每当浏览器遇到脚本标签时,DOM 构造就会暂停!整个 DOM 构建过程都将停止,直到脚本执行完成

这是因为 JavaScript 可以同时修改 DOM 和 CSSOM。由于浏览器不确定特定的 JavaScript 会做什么,所以它采取的预防措施是停止整个 DOM 构造。

<!DOCTYPE html>
<html>

<head>
    <meta name="viewport" content="width=device-width,initial-scale=1">
    <title>Medium Article Demo</title>
    <link rel="stylesheet" href="style.css">
</head>

<body>

    <p id="header">How Browser Rendering Works</p>
    <div><img src="https://i.imgur.com/jDq3k3r.jpg"></div>

    <script>
        let header = document.getElementById("header");

        console.log("header is: ", header);
    </script>
</body>

</html>

在脚本标签中,我将访问 id 为 header 的 DOM 节点,然后将其输出到控制台。可以正常运行。

但是,你是否注意到,这个脚本标签位于 body 标签的底部?让我们把它放在 head 中,看看会发生什么:一旦我这样做,header 就解析为 null。

为什么会这样?很简单。当 HTML 解析器正在构建 DOM 时,发现了一个脚本标签。此时,body 标签及其所有内容还没有被解析。DOM 构造将停止,直到脚本执行完成:

当脚本试图访问一个 id 为 header 的 DOM 节点时,由于 DOM 还没有完成对文档的解析,所以它还不存在。这把我们带到了另一个重要的问题。脚本的位置很重要。

这还不是全部。如果你将内联脚本提取到外部本地文件 app.js 中,行为是一样的。DOM 的构建仍然会停止:

<!DOCTYPE html>
<html>

<head>
    <meta name="viewport" content="width=device-width,initial-scale=1">
    <title>Medium Article Demo</title>
    <link rel="stylesheet" href="style.css">
    <script src="app.js"></script>
</head>

<body>

    <p id="header">How Browser Rendering Works</p>
    <div><img src="https://i.imgur.com/jDq3k3r.jpg"></div>
</body>

</html>

那么,如果 app.js 不是本地的而必须通过互联网获取呢?如果网速很慢,需要数千毫秒来获取 app.js,DOM 的构建也会暂停几千毫秒!!!这是一个很大的性能问题,而且还不止于此。JavaScript 还可以访问 CSSOM 并对其进行修改。例如,这是有效的 JavaScript 语句:

document.getElementsByTagName("body")[0].style.backgroundColor = "red";

那么,当解析器遇到一个脚本标签而 CSSOM 还没有准备好时,会发生什么情况呢?答案很简单。Javascript 执行将会停止,直到 CSSOM 就绪。因此,虽然 DOM 构造在遇到脚本标签时会停止,但 CSSOM 不会发生这种情况。对于 CSSOM,JS 执行会等待。没有 CSSOM,就没有 JS 执行

async 属性

在默认情况下,每个脚本都是一个解析器阻断器!DOM 的构建总是会被打断。不过,有一种方法可以改变这种默认行为。如果将 async 关键字添加到脚本标签中,那么 DOM 构造就不会停止。DOM 构造将继续,脚本将在下载完成并准备就绪后执行

<!DOCTYPE html>
<html>

<head>
    <meta name="viewport" content="width=device-width,initial-scale=1">
    <title>Medium Article Demo</title>
    <link rel="stylesheet" href="style.css">
    <script src="https://some-link-to-app.js" async></script>
</head>

<body>

    <p id="header">How Browser Rendering Works</p>
    <div><img src="https://i.imgur.com/jDq3k3r.jpg"></div>
</body>

</html>

关键渲染路径

目前为止,我们讨论了从接收 HTML、CSS 和 JS 字节到将它们转换为屏幕上的像素之间的所有步骤。这整个过程称为关键渲染路径。优化网站性能就是优化关键渲染路径。

一个经过良好优化的站点应该能够渐进式渲染,而不是让整个过程受阻。

这是 Web 应用程序慢或快的区别所在。周密的关键渲染路径(CRP)优化策略使浏览器能够通过确定优先加载的资源以及资源加载的顺序来尽可能快地加载页面。

常见引起回流属性和方法

任何会改变元素几何信息(元素的位置和尺寸大小)的操作,都会触发回流,

  • 添加或者删除可见的DOM元素;
  • 元素尺寸改变——边距、填充、边框、宽度和高度
  • 内容变化,比如用户在input框中输入文字
  • 浏览器窗口尺寸改变——resize事件发生时
  • 计算 offsetWidth 和 offsetHeight 属性
  • 设置 style 属性的值

常见引起重绘属性和方法

下面例子中,触发了几次回流和重绘?

var s = document.body.style;
s.padding = "2px"; // 回流+重绘
s.border = "1px solid red"; // 再一次 回流+重绘
s.color = "blue"; // 再一次重绘
s.backgroundColor = "#ccc"; // 再一次 重绘
s.fontSize = "14px"; // 再一次 回流+重绘
// 添加node,再一次 回流+重绘
document.body.appendChild(document.createTextNode('abc!'));

如何减少回流、重绘

  • 使用 transform 替代 top
  • 使用 visibility 替换 display: none ,因为前者只会引起重绘,后者会引发回流(改变了布局)
  • 不要把节点的属性值放在一个循环里当成循环里的变量。
  • 不要使用 table 布局,可能很小的一个小改动会造成整个 table 的重新布局
  • 动画实现的速度的选择,动画速度越快,回流次数越多,也可以选择使用 requestAnimationFrame
  • CSS 选择符从右往左匹配查找,避免节点层级过多
  • 将频繁重绘或者回流的节点设置为图层,图层能够阻止该节点的渲染行为影响别的节点。比如对于 video 标签来说,浏览器会自动将该节点变为图层。

async和defer的作用是什么?有什么区别?

接下来我们对比下 defer 和 async 属性的区别:

(1)情况1<script src="script.js"></script>

没有 defer 或 async,浏览器会立即加载并执行指定的脚本,也就是说不等待后续载入的文档元素,读到就加载并执行。

(2)情况2<script async src="script.js"></script> (异步下载)

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

(3)情况3 <script defer src="script.js"></script>(延迟执行)

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

defer 与相比普通 script,有两点区别:载入 JavaScript 文件时不阻塞 HTML 的解析,执行阶段被放到 HTML 标签解析完成之后。在加载多个JS脚本的时候,async是无顺序的加载,而defer是有顺序的加载。

为什么操作 DOM 慢

因为 DOM 是属于渲染引擎中的东西,而 JS 又是 JS 引擎中的东西。当我们通过 JS 操作 DOM 的时候,其实这个操作涉及到了两个线程之间的通信,那么势必会带来一些性能上的损耗。操作 DOM 次数一多,也就等同于一直在进行线程之间的通信,并且操作 DOM 可能还会带来重绘回流的情况,所以也就导致了性能上的问题。

渲染页面时常见哪些不良现象?

由于浏览器的渲染机制不同,在渲染页面时会出现两种常见的不良现象----白屏问题和FOUS(无样式内容闪烁)

FOUC:由于浏览器渲染机制(比如firefox),再CSS加载之前,先呈现了HTML,就会导致展示出无样式内容,然后样式突然呈现的现象;

白屏:有些浏览器渲染机制(比如chrome)要先构建DOM树和CSSOM树,构建完成后再进行渲染,如果CSS部分放在HTML尾部,由于CSS未加载完成,浏览器迟迟未渲染,从而导致白屏;也可能是把js文件放在头部,脚本会阻塞后面内容的呈现,脚本会阻塞其后组件的下载,出现白屏问题。

总结

  1. 浏览器工作流程:构建DOM -> 构建CSSOM -> 构建渲染树 -> 布局 -> 绘制。
  2. 当浏览器接收到原始数据字节并启动 DOM 构建过程时,它还会发出请求来获取链接的 main.css 样式表,启动CSSOM构建
  3. 构建DOM的过程中,不是等所有Token都转换完成后再去生成节点对象,而是一边生成Token一边消耗Token来生成节点对象。换句话说,每个Token被生成后,会立刻消耗这个Token创建出节点对象
  4. DOM 和 CSSOM 树结构是两个独立的树结构。DOM 中包含关于页面 HTML 元素关系的所有信息,而 CSSOM 则包含关于元素样式的信息。
  5. 浏览器得递归 CSSOM 树,然后确定具体的元素到底是什么样式,注意:CSS匹配HTML元素是一个相当复杂和有性能问题的事情。所以,DOM树要小,CSS尽量用id和class,千万不要过渡层叠下去
  6. CSSOM会阻塞渲染,只有当CSSOM构建完毕后才会进入下一个阶段构建渲染树。
  7. 渲染树包含页面上所有关于可见 DOM 内容的信息以及不同节点所需的所有 CSSOM 信息,隐藏元素会出现在 DOM 中,但不会出现在渲染树中。这是因为渲染树结合了来自 DOM 和 CSSOM 的信息,所以它知道不能把隐藏元素包含在树中。
  8. 构建好渲染树之后,浏览器必须计算页面上每个对象的确切大小和位置,这个布局步骤考虑了从 DOM 和 CSSOM 接收到的内容和样式,并执行了所有必要的布局计算(回流或者自动重排)。
  9. 每当浏览器遇到脚本标签时,DOM 构造就会暂停!整个 DOM 构建过程都将停止,但 CSSOM 不会发生这种情况,直到脚本执行完成,当解析器遇到一个脚本标签而 CSSOM 还没有准备好时,Javascript 执行将会停止,直到 CSSOM 就绪,对于 CSSOM,JS 执行会等待。
  10. 通常情况下DOM和CSSOM是并行构建的,但是当浏览器遇到一个script标签时,DOM构建将暂停,直至脚本完成执行。但由于JavaScript可以修改CSSOM,所以需要等CSSOM构建完毕后再执行JS。
  11. JS文件不只是阻塞DOM的构建,它会导致CSSOM也阻塞DOM的构建。原本DOM和CSSOM的构建是互不影响,井水不犯河水,但是一旦引入了JavaScript,CSSOM也开始阻塞DOM的构建,只有CSSOM构建完毕后,DOM再恢复DOM构建。在这种情况下,浏览器会先下载和构建CSSOM,然后再执行JavaScript,最后在继续构建DOM。
  12. 如果你想首屏渲染的越快,就越不应该在首屏就加载 JS 文件,建议将 script 标签放在 body 标签底部。
  13. 重绘:当render tree中的一些元素需要更新属性,而这些属性只是影响元素的外观、风格,而不会影响布局的,比如background-color。
  14. 回流:当render tree中的一部分(或全部)因为元素的规模尺寸、布局、隐藏等改变而需要重新构建
  15. 回流必定会发生重绘,重绘不一定会引发回流。重绘和回流会在我们设置节点样式时频繁出现,同时也会很大程度上影响性能。回流所需的成本比重绘高的多,改变父节点里的子节点很可能会导致父节点的一系列回流。

参考文档

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,013评论 6 481
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,205评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 152,370评论 0 342
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,168评论 1 278
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,153评论 5 371
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,954评论 1 283
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,271评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,916评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,382评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,877评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,989评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,624评论 4 322
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,209评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,199评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,418评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,401评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,700评论 2 345

推荐阅读更多精彩内容

  • 大家都知道万维网的应用层使用了HTTP协议,并且用浏览器作为入口访问网络上的资源。用户在使用浏览器访问一个网站时需...
    SylvanasSun阅读 2,139评论 1 12
  • 浏览器指的是Chrome系浏览器【Firefox大同小异,IE未知】以下提到的“节点”、“标签”和“元素”不做区分...
    Yieiy阅读 4,093评论 4 26
  • 关键渲染路径 浏览器从接收到页面开始到页面显示,这整个过程中的所有步骤,称为关键渲染路径。用户看到页面实际上可以分...
    ZombieBrandg阅读 492评论 0 0
  • 亲爱的 枫叶又红了 红叶也疯了 正午 阳光好暖 放下吧 就晒太阳 就目空一切 就无所事事 就一事无成 就这样 呆在阳光里
    一片_枫叶阅读 247评论 0 0
  • 有个小男孩,暑假被麻麻糖衣加炮弹的哄骗去了新东方上幼小衔接课程,幼小衔接这个名词我是在五年前知道的,当时娃尚在襁褓...
    陈娟米奇阅读 574评论 1 6