从网络接口下载的HTML文件字节流,渲染引擎无法理解,需转换为其能理解的结构:DOM。(类似的,js和css代码,渲染引擎也无法理解)
DOM的作用:
- 从页面角度看,DOM是生成页面的基础数据结构,供生成页面;
- 从js脚本角度看,js可以通过DOM提供的接口改变DOM,进而影响页面显示;
- 从安全角度看,DOM可以检查并隔绝部分安全隐患。
一、 DOM树生成
首先浏览器进程或者渲染进程会发起主页面请求,请求到的html文件,渲染引擎中,**HTML解析器(HTMLParser)**负责将HTML字节流转换为DOM树。

- 网络进程执行页面请求,根据响应头的content-type字段判断返回文件是否为"text/html"文件;
- 浏览器为该请求选择或者创建一个渲染进程进行连接;
- 网络进程和渲染进程建立共享数据的管道进行数据传递,渲染进程一边读取数据,一边通过HTML解析器解析已读取到的字节流。

渲染进程从网络进程接收到的是字节流形式,HTML解析器大致通过三个阶段进行转换:
-
通过分词器,将字节流转换为Token;
在编程语言中,Token是构成代码的最小单元,可以是单个字符或字符串。JS代码中,机器也不能理解js代码,所以V8引擎通过词法解析将代码分解为多个Token然后进行后面的操作。
而在HTML解析过程中,分词器会将字节流转换为不同类型的token,主要包括:标签Token(tag Token)和文本Token(text Token)。tag Token分为startTag和endTag。
javascript
<html>
<body>
<div>end</div>
</body>
</html>
这段js代码分解出的startTag和endTag如上图所示,div中的"end"为文本Token。
-
将Token解析为Dom节点并添加到DOM树;
HTML解析器维护了一个Token的栈结构 ,该栈主要用来计算节点之间的父子关系,第一阶段生成的Token会根据规则进出栈。HTML解析器开始工作时,默认创建根为document的空的DOM结构压在栈底。出入栈的简要规则为:
- 如果分词器解析出来是startTag Token,压入栈中,HTMLParser为该Token创建DOM节点并添加到DOM树,它的父节点就是栈中相邻的元素生成的节点;
- 如果分词器解析出来是文本Token,无需入栈,HTMLParser为该Token创建DOM节点并添加到DOM树,它的父节点就是栈中栈顶的元素生成的节点;
- 如果分词器解析出来是endTag Token,HTMLParser检查Token栈顶是否为对应的startTag Token,如果是,将startTag Token从栈顶弹出,表示该元素解析完成。
二. js和css对DOM解析过程的影响
在把HTML字节流解析成DOM时,过程中如果遇到js、css的代码或文件,将对DOM解析造成不同程度的影响。
2.1 css文件
javascript
<html>
<head>
<link rel="stylesheet" href="./test.css">
</head>
<body>
<div>origin</div>
<div>end</div>
</body>
</html>
<!-- test.css -->
div {
color: red;
}
这段HTML文件中增加了css文件,加载过程如下:
我们看到单独的css文件下载和解析并没有阻塞HTML解析器的解析。在DOMContentLoaded事件后进行样式计算和布局。这段程序的渲染流水线为:

- 渲染进程中接收HTML文件时,HTML解析器对已接收的字节流解析并构建DOM。
- css和html一样,渲染引擎无法理解并操作,需要将css代码解析为CSSOM。(CSSOM不仅给js提供了操作样式表的能力,还为布局树的合成提供了基础的样式信息,在DOM中可以通过document.styleSheets访问)
- DOM和CSSOM构建布局树。(首先布局树复制DOM的结构 ,DOM树中像display:none属性的元素、head和script标签等不需要显示的元素会被过滤掉。布局树结构复制完成以后,进行样式计算 :为对应的DOM节点选择对应的样式信息。然后进行计算布局:渲染引擎计算布局树中每个元素对应的几何位置。样式计算和计算布局完成后,布局树的构建就完成了。)
- 最后进行页面的绘制渲染。
第一步中,因为下载代码中引用的css、js等文件需要时间,这里chrome主要的优化为预解析操作:当渲染引擎接收到字节流后,会开启一个预解析的线程,分析文件中是否包含js、css等文件,提前进行下载。
从上图中我们看到,渲染流水线中有两个空闲时间: (1)发生在网络进程请求HTML文件还未返回之前 (2)发生在DOM树构建完成之后,网络进程请求CSS文件还未返回之前。下一步就是合成布局树了,需要css解析成CSSOM。
2.2 js内嵌脚本
javascript
<html>
<body>
<div>origin</div>
<script>
for (var i = 0, arr = []; i < 100000000; i++) {
arr.push(i)
}
console.log(arr)
</script>
<div>end</div>
</body>
</html>
这段代码执行效果和结果如下:
这段HTML文件中插入了一段js脚本,js脚本的执行阻塞了页面的渲染。这点和css脚本解析不同。这段代码的解析流程为:

<script>
标签之前解析过程还是一样正常解析为DOM,解析到<script>
这行代码时,渲染引擎判断该段为js脚本,HTML解析器会暂停DOM的解析,因为js脚本中可能含有对已生成DOM结构的修改。V8会对js脚本进行词法和语法解析等操作进行解析,生成AST。js脚本执行完成后,HTML解析器恢复解析剩余的字节流,直至生成最终的DOM。
2.2 js文件
相比于代码中的js内嵌脚本,引入js文件会相对复杂。
javascript
<html>
<body>
<div>origin</div>
<script type="text/javascript" src="./test.js"></script>
<div>end</div>
</body>
</html>
<!-- test.js -->
<script>
for (var i = 0, arr = []; i < 100000000; i++) {
arr.push(i)
}
console.log(arr)
</script>
此段HTML代码中js脚本通过js文件加载,<script>
标签之前解析过程还是一样,解析到<script>
时,需要先下载js文件,同样会阻塞HTML解析器对DOM的解析。
和对css文件处理相同,这里chrome也会进行预解析操作 :对js文件提前进行下载,下载时间会受到网络环境和文件大小的影响。我们也可以根据情况使用async和defer对js脚本进行异步加载。但是使用async属性标注的脚本,会在文件加载完成后立即执行,即在DOMContentLoaded事件之前或之后都有可能,如果在该事件之前,依然会阻塞DOM的执行。
执行的流程为:
2.3 css和js共同作用
javascript
<html>
<head>
<link rel="stylesheet" href="./test.css">
</head>
<body>
<div>origin</div>
<script>
for (var i = 0, arr = []; i < 100000000; i++) {
arr.push(i)
}
console.log(arr)
</script>
<div>end</div>
</body>
</html>
这段代码同时包含了css文件和js内嵌脚本。
因为V8在解析js脚本之前,不知道js脚本是否对CSSOM进行了操作,所以渲染引擎在遇到js脚本时,无论其是否操作了CSSOM,都会先下载并解析CSS文件。生成CSSOM之后,才能执行js脚本。
所以在这种情况下,CSS文件下载和解析间接对DOM的解析和生成造成阻塞。上文也说过,DOM也需要等待JS脚本执行,因为JS脚本可能会对当前已生成的DOM结构进行修改。因此,此段代码的解析流程为:
- HTML解析器解析HTML字节流生成DOM树,渲染引擎开启预解析线程解析到文件中有css文件,提前下载。
- 解析
<div>origin</div>
生成DOM树。 - 遇到
<script>
脚本,暂停DOM树解析,等待css文件下载完成并解析成CSSOM。 - CSSOM生成完成,解析并执行JS脚本。
- 恢复DOM树解析,对
<div>end</div>
进行解析。 - DOM树构建完成,和CSSOM树结合为render树,渲染页面。
如果css和js都是引用的外部文件,流程也是一样的。渲染引擎也会开启一个预解析的线程,下载css和js文件。但是不管哪个文件先下载完,还是需要先对css文件进行解析并生成CSSOM,再执行js脚本,最后继续构建DOM树,构建布局树,渲染页面。
三、白屏的优化
从url请求开始,页面展示原本的内容; 在服务器返回数据之后,渲染进程就会创建一个空白页面,就是解析白屏,等待css和js文件加载并解析完成,生成CSSOM和DOM,生成render树,进行页面渲染。 通常比较影响渲染的环节就是css、js文件下载和执行,优化策略为:
- 尽量通过内联css、js脚本代替文件下载,这样HTML文件获取到之后直接可以解析渲染。
- 不能内联的文件,尽量降低文件大小。删掉不必要的注释、压缩等方法。
- 不操作DOM的脚本通过async进行标注异步下载。
- 对于文件较大的css文件,通过媒体查询属性拆分成多个不同用途的css文件,在特定的场景下加载特定的css文件。
- 使用 CDN 来加速 JavaScript 文件的加载,压缩 JavaScript 文件的体积
文章参考:time.geekbang.org/column/intr... juejin.cn/post/747465...