什么是DOM
从⽹络传给渲染引擎的HTML⽂件字节流是⽆法直接被渲染引擎理解的,所以要将其转化为渲染引擎能够理 解的内部结构,这个结构就是DOM。
DOM有三个 层⾯的作⽤。
- 从⻚⾯的视⻆来看,DOM是⽣成⻚⾯的基础数据结构。
- 从JavaScript脚本视⻆来看,DOM提供给JavaScript脚本操作的接⼝,通过这套接⼝,JavaScript可以对 DOM结构进⾏访问,从⽽改变⽂档的结构、样式和内容。
- 从安全视⻆来看,DOM是⼀道安全防护线,⼀些不安全的内容在DOM解析阶段就被拒之⻔外了。
DOM树如何⽣成
在渲染引擎内部,有⼀个叫HTML解析器(HTMLParser)的模块,它的职责就是负责将HTML字节流转换 为DOM结构。
HTML解析器并不是等整个⽂档加载完成之后再解析的,⽽是⽹络进程加载了多少数据,HTML解析器便解析多少数据。
⽹络进程接收到响应头之后,会根据请求头中的content-type字段来判断⽂件的 类型,⽐如content-type的值是“text/html”,那么浏览器就会判断这是⼀个HTML类型的⽂件,然后为该 请求选择或者创建⼀个渲染进程。
渲染进程准备好之后,⽹络进程和渲染进程之间会建⽴⼀个共享数据的管 道,⽹络进程接收到数据后就往这个管道⾥⾯放,⽽渲染进程则从管道的另外⼀端不断地读取数据,并同时 将读取的数据“喂”给HTML解析器。HTML解析器它会动态接收字节流,并将其解析为DOM。
第⼀个阶段,通过分词器将字节流转换为Token。后续的第⼆个和第三个阶段是同步进⾏的,需要将Token解析为DOM节点,并将DOM节点添加到DOM 树中
<html>
<body>
<div>1</div>
<div>test</div>
</body>
</html>
HTML解析器开始⼯作时,会默认创建了⼀个根为document的空DOM结构,同时 会将⼀个StartTag document的Token压⼊栈底。然后经过分词器解析出来的第⼀个StartTag html Token会 被压⼊到栈中,并创建⼀个html的DOM节点,添加到document上

接下来解析出来的是第⼀个div的⽂本Token,渲染引擎会为该Token创建⼀个⽂本节点,并将该Token添加 到DOM中,它的⽗节点就是当前Token栈顶元素对应的节点

再接下来,分词器解析出来第⼀个EndTag div,这时候HTML解析器会去判断当前栈顶的元素是否是 StartTag div,如果是则从栈顶弹出StartTag div
最终解析

JavaScript是如何影响DOM⽣成的
<html>
<body>
<div>1</div>
<script>
let div1 = document.getElementsByTagName('div')[0]
div1.innerText = 'time.geekbang'
</script>
<div>test</div>
</body>
</html>
解析到<script>
标签时,渲染引擎判断这是⼀段脚本,此时 HTML解析器就会暂停DOM的解析,因为接下来的JavaScript可能要修改当前已经⽣成的DOM结构。
解析到<script>
时候的DOM树

这时候HTML解析器暂停⼯作,JavaScript引擎介⼊,并执⾏script标签中的这段脚本,因为这段JavaScript 脚本修改了DOM中第⼀个div中的内容,所以执⾏这段脚本之后,div节点内容已经修改为time.geekbang 了。脚本执⾏完成之后,HTML解析器恢复解析过程,继续解析后续的内容,直⾄⽣成最终的DOM。
如果是通过引入js文件的方式的话
//foo.js
let div1 = document.getElementsByTagName('div')[0]
div1.innerText = 'time.geekbang'
<html>
<body>
<div>1</div>
<script type="text/javascript" src='foo.js'></script>
<div>test</div>
</body>
</html>
执⾏到JavaScript标签时,暂停整个DOM的解析,先进行文件的下载。
这⾥需要重点关注下载环境,因为 JavaScript⽂件的下载过程会阻塞DOM解析,⽽通常下载⼜是⾮常耗时的,会受到⽹络环境、JavaScript ⽂件⼤⼩等因素的影响。
道引⼊JavaScript线程会阻塞DOM,不过也有⼀些相关的策略来规避,⽐如压缩JavaScript⽂件的体积、⽤CDN来加速JavaScript⽂件的加载。
如果JavaScript⽂件中没有操作 DOM相关代码,就可以将该JavaScript脚本设置为异步加载,通过async 或defer来标记代码
async和defer虽然都是异步的,不过还有⼀些差异,使⽤async标志的脚本⽂件⼀旦加载完成,会⽴即执 ⾏;⽽使⽤了defer标记的脚本⽂件,需要等到DOMContentLoaded事件之后执⾏。
CSS如何影响⾸次加载时的⽩屏时间?
//theme.css
div{
color : coral;
background-color:black
}
<html>
<head>
<link href="theme.css" rel="stylesheet">
</head>
<body>
<div>geekbang com</div>
</body>
</html>
渲染流⽔线⽰意图:

和HTML⼀样,渲染引擎也是⽆法直接理解CSS⽂件内容的,所以需要将其解析成渲染引擎能够理解的结 构,这个结构就是CSSOM。
CSSOM也具有两个作⽤
第⼀个是提供给JavaScript操作样式表 的能⼒
第⼆个是为布局树的合成提供基础的样式信息
构建信息
影响⻚⾯展⽰的因素以及优化策略
渲染流⽔线影响到了⾸次⻚⾯展⽰的速 度,⽽⾸次⻚⾯展⽰的速度⼜直接影响到了⽤⼾体验
从发起URL请求开始,到⾸次显⽰⻚⾯的内容,在视觉上经历的三个阶段
- 第⼀个阶段,等请求发出去之后,到提交数据阶段,⻚⾯展⽰出来的还是之前⻚⾯的内容。
- 第⼆个阶段,提交数据之后渲染进程会创建⼀个空⽩⻚⾯,我们通常把这段时间称为解析⽩屏,并等待 CSS⽂件和JavaScript⽂件的加载完成,⽣成CSSOM和DOM,然后合成布局树,最后还要经过⼀系列的 步骤准备⾸次渲染。
- 第三个阶段,等⾸次渲染完成之后,就开始进⼊完整⻚⾯的⽣成阶段了,然后⻚⾯会⼀点点被绘制出来。
影响白屏时间主要是下载CSS⽂件、下载JavaScript⽂件和执⾏JavaScript
想缩短⽩屏时⻓,可以有以下策略:
- 通过内联JavaScript、内联CSS来移除这两种类型的⽂件下载,这样获取到HTML⽂件之后就可以直接开 始渲染流程了。
- 可以尽量减少⽂件⼤⼩,⽐如通过webpack等⼯具移除⼀些不 必要的注释,并压缩JavaScript⽂件。
- 可以将⼀些不需要在解析HTML阶段使⽤的JavaScript标记上sync或者defer。
- 对于⼤的CSS⽂件,可以通过媒体查询属性,将其拆分为多个不同⽤途的CSS⽂件,这样只有在特定的场 景下才会加载特定的CSS⽂件。