DOM树:JavaScript是如何影响DOM树构建的?
DOM的作用
从网络进程传给渲染进程的html文件字节流无法直接被渲染引擎理解,需要将其转换为dom结构。因此,我们说dom提供了对html文档的结构化表述。
- 对页面来说,DOM是生成页面的基础数据结构;
- 对JS来说,DOM给JS脚本提供接口,使得JS可以通过这套接口对DOM结构进行访问,从而改变文档的结构,样式及内容;
- 从安全视角来看,一些不安全的内容在DOM解析的时候即可避免;
DOM树如何生成
渲染引擎中有个html解析器(HTMLParser)模块,其责任失去将HTML字节流转为DOM结构。 HTML 解析器并不是等整个文档加载完成之后再解析的,而是网络进程加载了多少数据,HTML 解析器便解析多少数据: 在网络进程收到响应头后,若响应头中的content-type是'text/html'的话,浏览器会为该请求选择或创建一个渲染进程,并向渲染进程发送“提交文档”的消息,在渲染进程接收到此消息后,将和网络进程建立一个数据共享的管道,这个管道类似一个水管,网络进程不断放数据,渲染进程不断读取数据并喂给html解析器。
具体过程
- 通过分词器将字节流转换为 Token:
- 第二个和第三个阶段是同步进行的,需要将 Token 解析为 DOM 节点,并将 DOM 节点添加到 DOM 树中。HTML 解析器维护了一个 Token 栈结构,该 Token 栈主要用来计算节点之间的父子关系,在第一个阶段中生成的 Token 会被按照顺序压到这个栈中。HTML 解析器开始工作时,会默认创建了一个根为 document 的空 DOM 结构。具体的处理规则如下所示:
- 如果压入到栈中的是 StartTag Token,HTML 解析器会为该 Token 创建一个 DOM 节点,然后将该节点加入到 DOM 树中,它的父节点就是栈中相邻的那个元素生成的节点。
- 如果分词器解析出来是文本 Token,那么会生成一个文本节点,然后将该节点加入到 DOM 树中,文本 Token 是不需要压入到栈中,它的父节点就是当前栈顶 Token 所对应的 DOM 节点。
- 如果分词器解析出来的是 EndTag 标签,比如是 EndTag div,HTML 解析器会查看 Token 栈顶的元素是否是 StarTag div,如果是,就将 StartTag div 从栈中弹出,表示该 div 元素解析完成。
JS如何影响DOM生成
<script>
标签
<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解析,因为js可能会修改当前生成的dom结构等。
- 引入js文件
<html>
<body>
<div>1</div>
<script type="text/javascript" src='foo.js'></script>
<div>test</div>
</body>
</html>
整体流程还是一样,遇到不同的是<script>
标签先暂停dom解析,需要先下载js文件在执行,因为js下载会堵塞dom的解析且受到网络,文件大小等多方面因素的影响。这方面Chrome浏览器也做了一些优化,比如预解析操作,当渲染引擎接收到字节流,会开启预解析线程,用来分析html中js、css文件,解析到相关文件,预解析线程会提前加载这些文件。除此之外,我们也可以通过CDN加速,压缩js文件大小,若js中没有涉及操作DOM,可以将脚本设置为异步加载,将代码标记为async/defer。其中,async标记的代码一旦加载完成会立即执行,而defer需要在DOMContentLoaded事件完成之前执行;
- js、css共存
//theme.css
div {color:blue}
<html>
<head>
<style src='theme.css'></style>
</head>
<body>
<div>1</div>
<script>
let div1 = document.getElementsByTagName('div')[0]
div1.innerText = 'time.geekbang' //需要DOM
div1.style.color = 'red' //需要CSSOM
</script>
<div>test</div>
</body>
</html>
js脚本含有操作CSSOM的代码,在js执行之前,需要解析js语句之上的css样式。因为代码又引用了外部的css文件,所在执行脚本之前,还需要等待外部的css文件下载完成,并解析生成CSSOM对象,在接着执行脚本。但是在渲染引擎解析脚本之前,对脚本是否操作CSSOM是未知的,所以渲染引擎在遇到脚本时,无论其是否涉及操作CSSOM的代码,都会先执行css文件的下载解析再执行脚本。
CSS如何影响首次加载时的白屏时间?
CSSOM的作用
- 提供给脚本操作样式表的能力
- 为布局树的合成提供基础样式信息
- 只有css下的渲染流水线
<html>
<head>
<link href="theme.css" rel="stylesheet">
</head>
<body>
<div>geekbang com</div>
</body>
</html>
- 请求 HTML 数据和构建 DOM 中间有一段空闲时间,这个空闲时间有可能成为页面渲染的瓶颈
- 当渲染进程接收 HTML 文件字节流时,会先开启一个预解析线程,预解析线程会解析出来一个外部的 theme.css 文件,并发起 theme.css 的下载。这里也有一个空闲时间就是在 DOM 构建结束之后、theme.css 文件还未下载完成的这段时间内,渲染流水线无事可做,因为下一步是合成布局树,而合成布局树需要 CSSOM 和 DOM,所以这里需要等待 CSS 加载结束并解析成 CSSOM。
<script>
标签脚本、CSS共存的渲染流水线
//theme.css
div{
color : coral;
background-color:black
}
<html>
<head>
<link href="theme.css" rel="stylesheet">
</head>
<body>
<div>geekbang com</div>
<script>
console.log('time.geekbang.org')
</script>
<div>geekbang com</div>
</body>
</html>
在执行脚本之前,渲染引擎需要先将外部css文件和
<style>
标签的css内容转换为CSSOM,因为JS有操作CSSOM的能力,也就是说CSS在部分情况下也会阻塞DOM的生成。
- 外部JS文件、CSS共存
预解析线程解析出有CSS、JS文件需要下载,将同时发起两个文件的下载请求,虽然下载过程是重叠,下载时间取决于长的那个。无论谁先到达,脚本都需要等css文件解析完并生成CSSOM后执行,在脚本执行后继续构建DOM,再构建布局树,绘制页面。
白屏时间及优化
白屏时间
在用户输入到首次显示页面内容的视觉变化主要分为三个阶段:
- 等请求发出去之后,到提交数据阶段,这段时间内页面仍然展示的是之前的页面内容
- 提交数据后渲染进程会创建一个空白页面,这段时间称之为解析白屏(白屏时间),并等待CSS和JS文件加载文成,并生成DOM和CSSOM,然后合成布局树,最后经过一系列步骤准备首次渲染。
- 等首次渲染完成之后,就进入完整页面的生成阶段,然后页面会被一点点绘制。
优化
白屏时间的瓶颈主要体现在下载 CSS 文件、下载 JavaScript 文件和执行 JavaScript; 所以要想缩短白屏时长,可以有以下策略:
- 内联 JavaScript、内联 CSS 来移除这两种类型的文件下载,这样获取到 HTML 文件之后就可以直接开始渲染流程了,但并不是所有的场合都适合内联。
- 尽量减少文件大小,比如通过 webpack 等工具移除一些不必要的注释,并压缩 JavaScript 文件。
- 将一些不需要在解析 HTML 阶段使用的 JavaScript 标记上 async 或者 defer。
- 利用媒体查询属性,在特定的场景下加载特定的 CSS 文件。
学习例子
1:<script src="foo.js" type="text/javascript"></script>
2:<script defer src="foo.js" type="text/javascript"></script>
3:<script sync src="foo.js" type="text/javascript"></script>
4:<link rel="stylesheet" type="text/css" href="foo.css" />
5:<link rel="stylesheet" type="text/css" href="foo.css" media="screen"/>
6:<link rel="stylesheet" type="text/css" href="foo.css" media="print" />
7:<link rel="stylesheet" type="text/css" href="foo.css" media="orientation:landscape" />
8:<link rel="stylesheet" type="text/css" href="foo.css" media="orientation:portrait" />
- 下载JavaScript文件并执行同步代码,会阻塞页面渲染
- defer异步下载JavaScript文件,会在HTML解析完成之后执行,在DOMContentLoaded之前执行,不会阻塞DOM构建,会阻塞页面渲染
- async异步下载JavaScript文件,下载完成之后会立即执行,有可能会阻塞页面渲染
- 下载CSS文件,可能阻塞页面渲染
- media属性用于区分设备,screen表示用于有屏幕的设备,无法用于打印机、3D眼镜、盲文阅读机等,在题设手机条件下,会加载
- print用于打印预览模式或打印页面,这里不会加载,不会阻塞页面渲染
- orientation:landscape表示横屏,与题设条件一致,会加载
- orientation:portrait表示竖屏,这里不会加载,不会阻塞页面渲染