CSS样式表和JS脚本对DOM的解析以及渲染的影响

CSS样式表和JS脚本对DOM的解析以及渲染的影响

通常网页的 css 样式一般写在 <head> 标签内,而 js 脚本一般放在 <body> 标签的最底部或在外链的脚本上使用 deferasync 属性。这些都是为了优化样式或者脚本给 DOM 的解析或渲染所带来的影响,也是 HTML5 的最佳实践。下面就将结合具体的例子做具体的探讨

DOM parse 的简易流程

首先,我们需要先了解下一个包含外部 css 样式和外部 js 脚本的 HTML 网页载入和渲染的简易流程

  1. 浏览器主进程开始下载 html 文档的内容并由 GUI 渲染线程开始解析 DOM

  2. 遇到样式表文件 <link rel='stylesheet'>,交由浏览器主线程下载,并继续解析 DOM

  3. 遇到脚本文件 <script>,会暂停 GUI 线程对 DOM 进行解析,开始由浏览器主线程下载脚本文件

  4. 下载结束后交由 JS 引擎线程执行该脚本文件,且此时的脚本文件可以访问已经解析完的 DOM

  5. 脚本执行结束后,JS 引擎线程切换为 GUI 线程继续对 DOM 进行解析

  6. 整个 DOM 解析完成,触发 DOMContentLoaded 事件(在 IE8 中可用 document.readyState === interactive 模拟)

DOMContentLoaded 事件

上述过程也可以说明 DOMContentLoaded 事件触发时,DOM 已经完全加载并解析完毕了,此时的 HTML 文档的样式文件,图片文件,iframe页面的加载可能都未完成,但此时的 JavaScript 已经可以操作 DOM 完成一些功能了。但在绝大多数场景下,样式文件的载入会延迟 DOMContentLoaded 事件的触发,尤其是在 JS 进行一些操作 DOM 元素的位置或大小的场景。所以多数浏览器会在触发 DOMContentLoaded 这个事件时会考虑到外部样式文件的载入,通常如果样式文件位于脚本之前,则浏览器会认为该脚本的执行依赖于样式的载入,延迟该脚本的执行与 DOMContentLoaded 事件的触发。不同的浏览器的行为也是不同的。

在 HTML5 的标准中,DOMContentLoaded 就是一个纯 DOM 事件与样式表无关,但在脚本执行前,出现在 <script> 标签前的 <link rel='stylesheet'> 样式文件必须完全载入,脚本执行后后面的 DOM 会继续解析,解析完毕后触发 DOMContentLoaded 事件。

DOMContentLoaded 事件相近的还有 load 事件以及 document.onreadystatechange 事件:

  • load 事件在页面完全载入是触发,此时 DOM 已经完成渲染,所有的样式文件,图片文件,iframe页面都已完成加载与渲染。

  • document.onreadystatechange 该事件的触发条件是当 document.readyState 属性发生改变。该属性有三个值分别为 loading,此时 DOM 正在加载。interactive,DOM 完成加载和解析,但样式,图像,框架之类的资源仍在加载,可用其模拟 DOMContentLoaded 事件与 jquery ready 事件。complete,所有资源都已加载完成。DOM 完成渲染,可用其模拟 load 事件

CSS样式与DOM解析与渲染的关系

下面为一个 demo <body> 标签中的代码

1
2
3
4
5
6
7
8
9
10
11
12
13
<h2>HEAD1</h2>
<script>
function print() {
console.log('first script', document.querySelectorAll('h2'));
}
print();
setTimeout(print, 0);
</script>
<link rel="stylesheet" href="https://cdn.bootcss.com/bootstrap/4.0.0/css/bootstrap.min.css">
<h2>HEAD2</h2>
<script>
console.log('second script');
</script>

运行代码前需要在 chrome 开发者工具的 Network 一栏中,将Disable cacheThrottling 做相应的设置模拟较慢的网速来观察 DOM 的解析与渲染的过程
dev tool setting

代码运行结果如下:
sync css and dom parse render

首先第一个 <script> 标签里面的代码开始运行,控制台中打印出一个 <h2>,在一个 JS事件循环 后打印出了两个 <h2>,即外部样式表后的 DOM已经完成解析。而此刻浏览器窗口中的两个 <h2> 标签并没有显示出来,第二个 <script> 标签里面的代码也没运行,可以判断浏览器应该正在加载外链的 css 样式表。所以可以样式表会阻塞并延迟整个 DOM 的渲染,包括其后的脚本。但并不会阻塞后续 DOM 的解析。同样内联的样式表同样表现如此。

JS脚本与DOM解析与渲染的关系

1
2
3
4
5
6
7
8
9
10
11
12
13
<h2>HEAD1</h2>
<script>
function print() {
console.log('first script', document.querySelectorAll('h2'));
}
print();
setTimeout(print, 0);
</script>
<script src="https://cdn.bootcss.com/bootstrap/4.0.0/js/bootstrap.min.js"></script>
<h2>HEAD2</h2>
<script>
console.log('second script');
</script>

代码运行结果如下:
sync js and dom parse render

此时浏览器同样在加载外部脚本,控制台里打印出了一个的 <h2> 标签并且在一次事件循环中依旧只打印出了一个 <h2>,此时浏览器页面上已经显示了第一个 <h2> 标签里的 HEAD1。而第二个<h2> 标签里的 HEAD2需要等待外部脚本载入执行完毕后才会在浏览器窗口打印出。即外链的脚本的加载与执行并不影响其前面的 DOM 的解析和渲染,但是会阻塞后续的 DOM 的解析当然也包括渲染。而内联的脚本表现出会阻塞脚本前的 DOM 的渲染。

动态的插入样式表与脚本对DOM解析与渲染的影响

由于动态的插入外链的样式表和外链的脚本都涉及到了网络请求,所以都不会阻塞 DOM 的解析与渲染。而动态的插入内联的样式表与脚本并不涉及网络请求都会阻塞后续的 DOM 的解析与渲染。当然了未链接到 DOM 树的动态创建的样式表和脚本不管是外链还是内联的,都不会被下载,解析,或执行。

参考

document.readyState
document.onreadystatechange
DOMContentLoaded
GlobalEventHandlers.onload
异步渲染的下载和阻塞行为
CSS/JS对DOM渲染的影响