DOM树:JavaScript和css是如何影响DOM树构建和渲染的?

8 篇文章 0 订阅
1 篇文章 0 订阅

DOM树:JavaScript和css是如何影响DOM树构建和渲染的?

1、JavaScript和css会不会DOM树构建和渲染的

先做个总结,然后再进行具体的分析:

CSS不会阻塞DOM树的解析,但是会影响 JavaScript的运行,

JavaScript 会阻止DOM树的解析,

最终CSS(CSSOM)会影响DOM树的渲染,也可以说最终会影响布局树的生成(有的版本说是渲染树,差不多意思)

CSS不会阻塞DOM解析,但是会阻塞DOM渲染,严谨一点则是CSS会阻塞render tree的生成,进而会阻塞DOM的渲染。
JS会阻塞DOM解析,CSS会阻塞JS的执行
浏览器遇到

2、什么是 DOM和DOM 树如何生成

1、什么是 DOM

从网络传给渲染引擎的 HTML 文件字节流是无法直接被渲染引擎理解的,所以要将其转化
为渲染引擎能够理解的内部结构,这个结构就是 DOM。DOM 提供了对 HTML 文档结构
化的表述。

DOM树结构示意图:

在这里插入图片描述

在渲染引擎中,DOM 有三个层面的作用:

1、从页面的视角来看,DOM 是生成页面的基础数据结构。
2、从 JavaScript 脚本视角来看,DOM 提供给 JavaScript 脚本操作的接口,通过这套接
口,JavaScript 可以对 DOM 结构进行访问,从而改变文档的结构、样式和内容。
3、从安全视角来看,DOM 是一道安全防护线,一些不安全的内容在 DOM 解析阶段就被拒
之门外了。

简言之,DOM 是表述 HTML 的内部数据结构,它会将 Web 页面和 JavaScript 脚本连接
起来,并过滤一些不安全的内容。

2、DOM 树如何生成

渲染引擎内部,有一个叫HTML 解析器(HTMLParser)的模块,它的职责就是负责将HTML 字节流转换为 DOM 结构。所以这里我们需要先要搞清楚 HTML 解析器是怎么工作的。
在开始介绍 HTML 解析器之前,我要先解释一个大家在留言区问到过好多次的问题:
HTML 解析器是等整个 HTML 文档加载完成之后开始解析的,还是随着 HTML 文档边加载边解析的?
在这里我统一解答下,HTML 解析器并不是等整个文档加载完成之后再解析的,而是网络进程加载了多少数据,HTML 解析器便解析多少数据。

那详细的流程是怎样的呢?网络进程接收到响应头之后,会根据请求头中的 content-type字段来判断文件的类型,比如 content-type 的值是“text/html”,那么浏览器就会判断这是一个 HTML 类型的文件,然后为该请求选择或者创建一个渲染进程。渲染进程准备好之后,网络进程和渲染进程之间会建立一个共享数据的管道,网络进程接收到数据后就往这个管道里面放,而渲染进程则从管道的另外一端不断地读取数据,并同时将读取的数据“喂”给 HTML 解析器。你可以把这个管道想象成一个“水管”,网络进程接收到的字节流像水一样倒进这个“水管”,而“水管”的另外一端是渲染进程的 HTML 解析器,它会动态接收字节流,并将其解析为 DOM。

解答完这个问题之后,接下来我们就可以来详细聊聊 DOM 的具体生成流程了。

前面我们说过代码从网络传输过来是字节流的形式,那么后续字节流是如何转换为 DOM的呢?你可以参考下图:

字节流转换为 DOM

在这里插入图片描述

从图中你可以看出,字节流转换为 DOM 需要三个阶段。

1.字节流转换为 DOM 需要三个阶段

第一个阶段,通过分词器将字节流转换为 Token。

前面《14 | 编译器和解释器:V8 是如何执行一段 JavaScript 代码的?》文章中我们介绍
过,V8 编译 JavaScript 过程中的第一步是做词法分析,将 JavaScript 先分解为一个个
Token。解析 HTML 也是一样的,需要通过分词器先将字节流转换为一个个 Token,分为
Tag Token 和文本 Token。上述 HTML 代码通过词法分析生成的 Token 如下所示:

在这里插入图片描述

由图可以看出,Tag Token 又分 StartTag 和 EndTag,比如就是 StartTag ,就是EndTag,分别对于图中的蓝色和红色块,文本 Token 对应的绿色块。

至于后续的第二个和第三个阶段是同步进行的,需要将 Token 解析为 DOM 节点,并将DOM 节点添加到 DOM 树中。

HTML 解析器(HTMLParser)维护了一个Token 栈结构,该 Token 栈主要用来计算节点之间的父子关系,在第一个阶段中生成的 Token 会被按照顺序压到这个栈中。具体的处理规则如下所示:

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

通过分词器产生的新 Token 就这样不停地压栈和出栈,整个解析过程就这样一直持续下去,直到分词器将所有字节流分词完成。

2.DOM 树的生成过程案例

为了更加直观地理解整个过程,下面我们结合一段 HTML 代码(如下),来一步步分析DOM 树的生成过程。

<html>
<body>
    <div>1</div>
    <div>test</div>
</body>
</html>

这段代码以字节流的形式传给了 HTML 解析器,经过分词器处理,解析出来的第一个Token 是 StartTag html,解析出来的 Token 会被压入到栈中,并同时创建一个 html 的DOM 节点,将其加入到 DOM 树中。

这里需要补充说明下,HTML 解析器开始工作时,会默认创建了一个根为 document 的空 DOM 结构,同时会将一个 StartTag document 的 Token 压入栈底。然后经过分词器解析出来的第一个 StartTag html Token 会被压入到栈中,并创建一个 html 的 DOM 节点,添加到 document 上,如下图所示:

1、解析到 StartTag html 时的状态

在这里插入图片描述

然后按照同样的流程解析出来 StartTag body 和 StartTag div,其 Token 栈和 DOM 的状态如下图所示:

2、解析到 StartTag div 时的状态

在这里插入图片描述

接下来解析出来的是第一个 div 的文本 Token,渲染引擎会为该 Token 创建一个文本节点,并将该 Token 添加到 DOM 中,它的父节点就是当前 Token 栈顶元素对应的节点,如下图所示:

3、解析出第一个文本 Token 时的状态

在这里插入图片描述

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

4、元素弹出 Token 栈示意图

在这里插入图片描述

按照同样的规则,一路解析,最终结果如下图所示:

5、最终解析结果

在这里插入图片描述

通过上面的介绍,相信你已经清楚 DOM 是怎么生成的了。不过在实际生产环境中,HTML源文件中既包含 CSS 和 JavaScript,又包含图片、音频、视频等文件,所以处理过程远比上面这个示范 Demo 复杂。不过理解了这个简单的 Demo 生成过程,我们就可以往下分析更加复杂的场景了。

3、JavaScript对DOM树构建和渲染的影响

JavaScript对DOM树构建和渲染的影响,分成三种类型来讲解:

1、JavaScript 脚本在html页面中

案例:

<html>
<body>
<div>1</div>
<script>
let div1 = document.getElementsByTagName('div')[0]
div1.innerText = 'time.geekbang'
</script>
<div>test</div>
</body>
</html>

我在两段 div 中间插入了一段 JavaScript 脚本,这段脚本的解析过程就有点不一样了。

通过前面 DOM 生成流程分析,我们已经知道当解析到 script 脚本标签时,其 DOM 树结构如下所示:

在这里插入图片描述

这时候 HTML 解析器暂停工作,JavaScript 引擎介入,并执行 script 标签中的这段脚本,
因为这段 JavaScript 脚本修改了 DOM 中第一个 div 中的内容,所以执行这段脚本之后,
div 节点内容已经修改为 time.geekbang 了。脚本执行完成之后,HTML 解析器恢复解析过程,继续解析后续的内容,直至生成最终的 DOM。

以上过程应该还是比较好理解的,不过除了在页面中直接内嵌 JavaScript 脚本之外,我们还通常需要在页面中引入 JavaScript 文件,这个解析过程就稍微复杂了些,如下面案例:

2、html页面中引入 JavaScript 文件

案例:

//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 脚本修改成了通过 JavaScript 文件加载。其整个执行流程还是一样的,执行到 JavaScript 标签时,暂停整个 DOM 的解析,执行 JavaScript 代码,不过这里执行 JavaScript 时,需要先下载这段 JavaScript 代码。这里需要重点关注下载环境,因为JavaScript 文件的下载过程会阻塞DOM 解析,而通常下载又是非常耗时的,会受到网络环境、JavaScript 文件大小等因素的影响。

不过 Chrome 浏览器做了很多优化,其中一个主要的优化是预解析操作。当渲染引擎收到字节流之后,会开启一个预解析线程,用来分析 HTML 文件中包含的 JavaScript、CSS 等相关文件,解析到相关文件之后,预解析线程会提前下载这些文件。

再回到 DOM 解析上,我们知道引入 JavaScript 线程会阻塞 DOM,不过也有一些相关的策略来规避,比如使用 CDN 来加速 JavaScript 文件的加载,压缩 JavaScript 文件的体积。另外,如果 JavaScript 文件中没有操作 DOM 相关代码,就可以将该 JavaScript 脚本设置为异步加载,通过 async 或 defer 来标记代码,使用方式如下所示:

<script async type="text/javascript" src='foo.js'></script>
或者
<script defer type="text/javascript" src='foo.js'></script>

async:脚本并行加载,加载完成之后立即执行,执行时机不确定,仍有可能阻塞HTML解析,执行时机在load事件派发之前

defer:脚本并行加载,等待HTML解析完成之后,按照加载顺序执行脚本,执行时机在DOMContentLoaded事件派发之前

3、html页面中有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>

这段话很重要:

该示例中,JavaScript 代码出现了 div1.style.color = ‘red’ 的语句,它是用来操
纵 CSSOM 的,所以在执行 JavaScript 之前,需要先解析 JavaScript 语句之上所有的CSS 样式。所以如果代码里引用了外部的 CSS 文件,那么在执行 JavaScript 之前,还需要等待外部的 CSS 文件下载完成,并解析生成 CSSOM 对象之后,才能执行 JavaScript 脚本。

而 JavaScript 引擎在解析 JavaScript 之前,是不知道 JavaScript 是否操纵了 CSSOM的,所以渲染引擎在遇到 JavaScript 脚本时,不管该脚本是否操纵了 CSSOM,都会执行CSS 文件下载,解析操作,再执行 JavaScript 脚本。

所以说 JavaScript 脚本是依赖样式表的,这又多了一个阻塞过程。

通过上面的分析,我们知道了 JavaScript 会阻塞 DOM 生成,而样式文件又会阻塞
JavaScript 的执行,所以在实际的工程中需要重点关注 JavaScript 文件和样式表文件,使用不当会影响到页面性能的。

注意点:

1、额外说明一下,渲染引擎还有一个安全检查模块叫 XSSAuditor,是用来检测词法安全的。
在分词器解析出来 Token 之后,它会检测这些模块是否安全,比如是否引用了外部脚本,
是否符合 CSP 规范,是否存在跨站点请求等。如果出现不符合规范的内容,XSSAuditor会对该脚本或者下载任务进行拦截。详细内容我们会在后面的安全模块介绍,这里就不赘述了。

2、DOMContentLoaded事件将在页面DOM解析完成后触发。

4、CSS对DOM树构建和渲染的影响

1、渲染流水线视角下的 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 文件和 HTML 文件构成,我们来分析下打开这段 HTML 文件时的渲染流水线,你可以先参考下面这张渲染流水线示意图:

含有 CSS 的页面渲染流水线

在这里插入图片描述

面我们结合上图来分析这个页面文件的渲染流水线。

首先是发起主页面的请求,这个发起请求方可能是渲染进程,也有可能是浏览器进程,发起的请求被送到网络进程中去执行。网络进程接收到返回的 HTML 数据之后,将其发送给渲染进程,渲染进程会解析 HTML 数据并构建 DOM。这里你需要特别注意下,请求 HTML数据和构建 DOM 中间有一段空闲时间,这个空闲时间有可能成为页面渲染的瓶颈。

我们提到过,当渲染进程接收 HTML 文件字节流时,会先开启一个预解析线
程,如果遇到 JavaScript 文件或者 CSS 文件,那么预解析线程会提前下载这些数据。对于上面的代码,预解析线程会解析出来一个外部的 theme.css 文件,并发起 theme.css 的下载。这里也有一个空闲时间需要你注意一下,就是在 DOM 构建结束之后、theme.css 文件还未下载完成的这段时间内,渲染流水线无事可做,因为下一步是合成布局树,而合成布局树需要 CSSOM 和 DOM,所以这里需要等待 CSS 加载结束并解析成 CSSOM。

很多人肯定好奇,渲染流水线为什么需要 CSSOM 呢?

和 HTML 一样,渲染引擎也是无法直接理解 CSS 文件内容的,所以需要将其解析成渲染引
擎能够理解的结构,这个结构就是 CSSOM。和 DOM 一样,CSSOM 也具有两个作用,第
一个是提供给 JavaScript 操作样式表的能力,第二个是为布局树的合成提供基础的样式信
息。这个 CSSOM 体现在 DOM 中就是document.styleSheets。具体结构你可以去查
阅相关资料,这里我就不过多介绍了,你知道 CSSOM 的两个作用是怎样的就行了。

有了 DOM 和 CSSOM,接下来就可以合成布局树了,我们在前面《05 | 渲染流程
(上):HTML、CSS 和 JavaScript 文件,是如何变成页面的?》这篇文章中讲解过布局树的构造过程,这里咱们再简单回顾下。等 DOM 和 CSSOM 都构建好之后,渲染引擎就会构造布局树(有的版本称为渲染树)。

布局树的结构基本上就是复制 DOM 树的结构,不同之处在于 DOM 树中那些不需要显示的元素会被过滤掉,如 display:none 属性的元素、head 标签、script 标签等。复制好基本的布局树结构之后,渲染引擎会为对应的 DOM 元素选择对应的样式信息,这个过程就是样式计算。样式计算完成之后,渲染引擎还需要计算布局树中每个元素对应的几何位置,这个过程就是计算布局。通过样式计算和计算布局就完成了最终布局树的构建。再之后,就该进行后续的绘制操作了。

这就是在渲染过程中涉及到 CSS 的一些主要流程。

2、 body 标签内部加 JavaScript 代码

案例:

//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>

这段代码是我在开头代码的基础之上做了一点小修改,在 body 标签内部加了一个简单的JavaScript。有了 JavaScript,渲染流水线就有点不一样了,可以参考下面这张渲染流水线图:

含有 JavaScript 和 CSS 的页面渲染流水线

在这里插入图片描述

那我们就结合这张图来分析含有外部 CSS 文件和 JavaScript 代码的页面渲染流水线,上一篇文章中我们提到过在解析 DOM 的过程中,如果遇到了 JavaScript 脚本,那么需要先暂停 DOM 解析去执行 JavaScript,因为 JavaScript 有可能会修改当前状态下的 DOM。

不过在执行 JavaScript 脚本之前,如果页面中包含了外部 CSS 文件的引用,或者通过style 标签内置了 CSS 内容,那么渲染引擎还需要将这些内容转换为 CSSOM,因为JavaScript 有修改 CSSOM 的能力,所以在执行 JavaScript 之前,还需要依赖 CSSOM。
也就是说 CSS 在部分情况下也会阻塞 DOM 的生成。

3、body 中被包含的是 JavaScript 外部引用文件

案例:

//theme.css
div{
    color : coral;
    background-color:black
}

//foo.js
console.log('time.geekbang.org')

<html>
<head>
<link href="theme.css" rel="stylesheet">
</head>
<body>
    <div>geekbang com</div>
    <script src='foo.js'></script>
    <div>geekbang com</div>
</body>
</html>

HTML 文件中包含了 CSS 的外部引用和 JavaScript 外部文件,那它们的渲染流水线是怎样的呢?可参考下图:

含有 JavaScript 文件和 CSS 文件页面的渲染流水线

在这里插入图片描述

从图中可以看出来,在接收到 HTML 数据之后的预解析过程中,HTML 预解析器识别出来了有 CSS 文件和 JavaScript 文件需要下载,然后就同时发起这两个文件的下载请求,需要注意的是,这两个文件的下载过程是重叠的,所以下载时间按照最久的那个文件来算。

后面的流水线就和前面是一样的了,不管 CSS 文件和 JavaScript 文件谁先到达,都要先等到 CSS 文件下载完成并生成 CSSOM,然后再执行 JavaScript 脚本,最后再继续构建DOM,构建布局树,绘制页面。

5、影响页面展示的因素以及优化策略

前面我们为什么要花这么多文字来分析渲染流水线呢?主要原因就是渲染流水线影响到了首次页面展示的速度,而首次页面展示的速度又直接影响到了用户体验,所以我们分析渲染流水线的目的就是为了找出一些影响到首屏展示的因素,然后再基于这些因素做一些针对性的调整。

6、从发起 URL 请求开始,到首次显示页面的内容

1、一个阶段,等请求发出去之后,到提交数据阶段,这时页面展示出来的还是之前页面的内容。关于提交数据你可以参考前面《04 | 导航流程:从输入 URL 到页面展示,这中间发生了什么?》这篇文章。
2、第二个阶段,提交数据之后渲染进程会创建一个空白页面,我们通常把这段时间称为解析白屏,并等待 CSS 文件和 JavaScript 文件的加载完成,生成 CSSOM 和 DOM,然后合成布局树,最后还要经过一系列的步骤准备首次渲染。
3、第三个阶段,等首次渲染完成之后,就开始进入完整页面的生成阶段了,然后页面会一点点被绘制出来。

影响第一个阶段的因素主要是网络或者是服务器处理这块儿,前面文章中我们已经讲过了,这里我们就不再继续分析了。至于第三个阶段,我们会在后续文章中分析,所以这里也不做介绍了。

现在我们重点关注第二个阶段,这个阶段的主要问题是白屏时间,如果白屏时间过久,就会影响到用户体验。为了缩短白屏时间,我们来挨个分析这个阶段的主要任务,包括了解析HTML、下载 CSS、下载 JavaScript、生成 CSSOM、执行 JavaScript、生成布局树、绘制页面一系列操作。

常情况下的瓶颈主要体现在下载 CSS 文件、下载 JavaScript 文件和执行JavaScript。所以要想缩短白屏时长,可以有以下策略:

1、通过内联 JavaScript、内联 CSS 来移除这两种类型的文件下载,这样获取到 HTML 文件之后就可以直接开始渲染流程了。
2、但并不是所有的场合都适合内联,那么还可以尽量减少文件大小,比如通过 3、3、webpack 等工具移除一些不必要的注释,并压缩 JavaScript 文件。
还可以将一些不需要在解析 HTML 阶段使用的 JavaScript 标记上 sync 或者 defer。
4、对于大的 CSS 文件,可以通过媒体查询属性,将其拆分为多个不同用途的 CSS 文件,这样只有在特定的场景下才会加载特定的 CSS 文件。

通过以上策略就能缩短白屏展示的时长了,不过在实际项目中,总是存在各种各样的情况,这些策略并不能随心所欲地去引用,所以还需要结合实际情况来调整最佳方案。

参考文章:

1、DOM树:JavaScript是如何影响DOM树构建的?

https://time.geekbang.org/column/article/140140

2、渲染流水线:CSS如何影响首次加载时的白屏时间?

https://time.geekbang.org/column/article/140703

3、关于 JS 与 CSS 是否阻塞 DOM 的渲染和解析

https://juejin.cn/post/6973949865130885157

4、CSS 会阻塞 DOM 解析吗?

https://mp.weixin.qq.com/s?__biz=Mzg2NjUxOTM2Mg==&mid=2247486976&idx=1&sn=e50dcb2e110aa2d02bddaf8dcd1ffd95&chksm=ce48de2df93f573b8204ce4a06c5b33b49f5d6c58e67e00c455db003fa8d32f578366c573140&mpshare=1&scene=1&srcid=0707BmdIxYJFrkGCEPuqwZgV&sharer_sharetime=1625619585095&sharer_shareid=ed2c612b8d7f70a6495efc4f172d0d7d&version=3.1.8.3015&platform=win#rd

思考题:
1、预解析时候,css文件和js文件加载顺序?
2、浏览器是不是有一个尽量快速渲染页面的机制呢?比如说,有时候打开一个网页很慢,后面慢慢地显示了样式错乱的页面,这明显是css没有加载构建完成,但是还是看到有页面出来了,只是样式有点乱。
3、如果script放在 还有优化的意义么 这样就不会阻塞渲染了么?

4、async和defer在什么阶段执行

5、DOM tree 和 render tree 构建是同时进行的还是DOM tree和 cssom都构建完成后才构建render tree呢?

6、预解析,下载js文件的过程,会阻塞DOM树解析嘛?

  • 4
    点赞
  • 15
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值