JavaScript如何影响DOM树构建?

9/22/2022 阻塞DOM解析

# 前言

渲染流水线里第一个子阶段构建DOM树,后续的子阶段直接或间接都依赖DOM树结构。
构建流程:输入HTML文件 => HTML解析器解析 => 输出DOM树。

# DOM是什么?

网络进程传输给渲染进程的HTML文件字节流无法直接被渲染引擎理解,所以需要将其转换为渲染引擎能理解的内部结构,该结构就是DOM(Document-Object-Model/文档对象模型)。
DOM (opens new window)提供了对HTML文档结构化的表述,在渲染引擎里,DOM有三个层面的作用:

  • 从页面层看,DOM是生成页面的基础数据结构。
  • 从JavaScript脚本层看,DOM提供给JavaScript操作的接口,通过这套接口,JavaScript能对DOM结构进行访问、改变文档结构、样式、内容,将Web页面和脚本(程序语言)连接起来。
  • 从安全层看,DOM是一道安全防护线,DOM解析阶段就无法解析一些不安全的内容。

综上:DOM是表述HTML的内部数据结构,将Web页面和JavaScript脚本连接起来,并能过滤一些不安全的内容。

# DOM树如何生成?

渲染引擎内部,有一个HTML解析器(HTMLParser)模块,负责将HTML字节流转换为DOM结构。

# HTML解析器何时解析?

HTML解析器不是等整个文档加载完成之后再去解析,而是网络进程加载了多少数据,HTML解析器便解析多少数据
网络进程接收到响应头之后,根据响应头中的content-type字段来判断文件的类型,如果值是”text/html”,浏览器就会判断这是HTML类型的文件,然后会为该请求选择or创建一个渲染进程。渲染进程准备好后,网络进程和渲染进程之间会建立一个共享数据的管道,网络进程接收到字节流数据就会把数据放到管道,渲染进程则从管道的另外一端不断地读取数据,并同时将读取的数据送给HTML解析器,渲染进程的HTML解析器动态接收字节流,并将其解析为DOM。 HTML字节流转DOM

# 1.分词器将字节流转为Token => Token栈维护

V8引擎编译JavaScript过程中的第一步就是做词法分析,将JavaScript分解为一个个Token。解析HTML也是如此,需要通过分词器先将字节流转换为一个个Token

  • Tag Token(StartTag or EndTag) => startTag-html、endTag-html
  • 文本 Token => content

渲染引擎有一个安全检查模块XSSAuditor,用于检测词法安全。在分词器解析出Token之后,会检测这些模块是否安全,比如是否引用了外部脚本,是否符合CSP规范,是否存在跨站点请求等。如果出现不符合规范的内容,XSSAuditor会拦截该脚本或下载任务进行。

# 2.将Token解析为DOM节点 3.将DOM节点添加到DOM树中

这两步是同步进行的。HTML解析器维护了一个Token栈结构,Token栈主要用来计算节点之间的父子关系,在上一步中生成的Token,会按照顺序压到这个栈中。

  • 入栈:如果入栈的是StartTag Token,HTML解析器会为该Token创建一个DOM节点,将其加入到DOM树中,其父节点就是栈中相邻的元素生成的节点。
  • 出栈:如果分词器解析出来的是EndTag Token(EndTag div),HTML解析器会查看Token栈顶的元素是否是StarTag div,如果是,就将StartTag div从栈中弹出,表示该div元素解析完成。
  • 如果分词器解析出是文本 Token,那么会生直接成一个文本节点,将其加入到DOM树中,文本 Token是不需要压入到栈中的,其父节点就是当前栈顶Token所对应的DOM节点。

通过分词器产生的新Token就这样不停地压栈和出栈,整个解析过程就这样一直持续下去,直到分词器将所有字节流分词完成。
上述思路可以联想到这道题:有效的括号
在实际生产环境中,HTML源文件中可能包含CSS、JavaScript、图片、音频、视频等文件,所以处理过程比上述的还要复杂一点。

# JavaScript如何影响DOM生成?

<html>
<body>
    <div>content</div>
    <script>
      let d1 = document.getElementsByTagName('div')[0]
      // 需要依赖DOM
      d1.innerText = 'js-content'
    </script>
    <div>test</div>
</body>
</html>
1
2
3
4
5
6
7
8
9
10
11

HTML解析器遇到script标签会暂停DOM的解析,因为JavaScript有可能修改当前已经生成的DOM结构。然后JavaScript引擎介入,执行script标签中的脚本,修改了DOM中第一个div的内容,脚本执行完毕,HTML解析器恢复执行解析过程,继续解析后续内容,直至生成最终DOM树。

# 暂停DOM解析,下载执行JavaScript代码

一般情况,我们引入的这段JavaScript代码需要先下载,下载的过程会阻塞DOM解析,而且下载一般都是很耗时的(网络环境、JavaScript文件大小)。

# 优化阻塞DOM解析的策略

  1. Chrome浏览器的优化:预解析操作。当渲染引擎接收到字节流,会开启一个预解析线程,专门用于分析HTML文件中包含的JavaScript、CSS等相关的文件,一旦解析到这些文件,预解析线程会提前下载这些文件。
  2. CDN加速JavaScript文件下载。
  3. 压缩JavaScript文件体积。
  4. 如果JavaScript文件中并没有操作DOM的代码,可以将其设置为异步加载async、defer进行标记。
     // async => 并行请求下载该脚本,避免阻塞浏览器解析HTML。
     // 文件一旦加载完成,会立即执行
     <script async type="text/javascript" src='test.js'></script>
     // defer => HTML完成解析后在触发DOMContentLoaded事件之前执行 => 会阻止DOMContentLoaded事件
     <script defer type="text/javascript" src='test.js'></script>
    
    1
    2
    3
    4
    5

# CSS同样也阻塞了HTML解析过程

// 是依赖CSSOM的
div.style.color = 'red'
1
2

这是用于操纵CSSOM的,所以在执行JavaScript之前,需要解析JavaScript语句中所有的CSS样式。如果代码中引用了外部CSS文件,那么在执行JavaScript之前,还需等待外部CSS文件下载完成并且解析生成CSSOM对象之后,才能执行JavaScript脚本
JavaScript引擎在解析JavaScript之前,无法知道JavaScript是否操作了CSSOM,所以渲染引擎在遇到JavaScript脚本之前,不管脚本是否操作了CSSOM,都会执行CSS文件下载、解析的操作,再执行JavaScript脚本。
所以:JavaScript脚本是依赖样式表的。 => 这又是一个阻塞过程

# 总结

  • HTML字节流 => 分词器生成Token(栈维护) => Token解析为DOM节点 => DOM节点添加到DOM树
  • JavaScript阻塞DOM生成,CSS样式文件又阻塞JavaScript的执行。(需要关注JavaScript文件和样式表文件,影响页面性能)
Last Updated: 11/2/2022, 5:11:53 PM