CSS 和 JS 是否会阻塞 HTML 的解析?
“我报名参加金石计划1期挑战——瓜分10万奖池,这是我的第3篇文章,点击查看活动详情”
页面的渲染过程
- 在浏览器输入
URL
并按下回车后,服务端返回HTML
文档
- 浏览器自顶向下解析
HTML
文档,解析的过程中会根据解析到的内容构建DOM
树
- 解析到
style
标签(外联样式)会并行加载对应的资源,加载完成后解析样式构建样式规则,生成CSSOM
树(js
执行是单线程的,但浏览器并不是,所以其可以一边解析HTML
,一边解析CSS
)
- 解析到
script
标签(这里说的是最普通的,defer
和async
属性后续会有详细介绍)时会停止HTML
的解析并加载脚本,加载完成之后立即执行,随后HTML
继续进行解析
DOM
树和样式规则也就是CSSOM
树构建完成之后会进行合并,生成渲染树render tree
- 浏览器会对渲染树进行布局,目的是为了得到相关的布局信息,比如
DOM
元素在浏览器上的具体坐标和大小,最终生成布局树layout tree
- 浏览器会对布局树进行绘制和分层,将布局树转换为对应的像素信息,最终传递给
GPU
将页面绘制出来呈现给用户
JS 是否阻塞 HTML 的解析?
上文中也提到了,对于普通的 script
标签来说,它的加载和执行都会阻塞 HTML
的解析,那如果添加上了 async
或 defer
属性会有什么变化呢?下面先看一张图片:
绿色代表 HTML 解析阶段
蓝色代表通过网络请求加载脚本阶段
红色代表执行脚本阶段
普通的 script
标签和上述说的一样,加载和执行都会阻塞 HTML
的解析,除非将普通的脚本放到 HTML
文档底部,这样就不会阻塞 HTML
的解析,首屏渲染的速度也会更快。但是 defer
、async
这两个属性就不一样了,它们有个公共的地方就是对应的 js
文件在加载时并不会阻塞 HTML
的解析
但也有明显的区别,含有 defer
属性的脚本在加载完之后不会立即执行,而是会放到一个队列中,等到 HTML
解析完成之后依次取出来执行,所以 defer
属性对应的脚本有个明显的特点就是脚本的执行顺序和出现在 HTML
文档中的顺序一致,而且它一定会在 DOMContentLoaded
事件触发之前执行,该事件下文中会进行详细的讲解,下面是 MDN
中对该属性的说明:
这个布尔属性被设定用来通知浏览器该脚本将在文档完成解析后,触发
DOMContentLoaded (en-US)
事件前执行。 有defer
属性的脚本会阻止DOMContentLoaded
事件,直到脚本被加载并且解析完成。——MDN
含有 async
属性的脚本执行时机就没有 defer
要求那么严格,它的执行时机并不确定,可能是在 HTML
解析完成前,此时会立即停止 HTML
的解析并执行脚本;还可能是在 DOMContentLoaded
事件触发后,此时 HTML
已经解析完成了,这种情况是有可能发生的,因为脚本什么时候加载好并不是确定的,不知道你有没有发现,这种情况下脚本的执行就没有阻塞 HTML
的解析,所以说脚本执行一定会延迟 DOMContentLoaded
事件的触发时间是不严谨的,但是 async
脚本一定会在 load
事件触发之前执行完毕
下图为三种不同类型脚本的执行时机,普通脚本一定会在 HTML
解析完成之前执行;defer
脚本一定会在 HTML
解析完成之后、DOMContentLoaded
事件触发之前执行完毕;async
脚本可能会在任意情况下执行,但一定会在 load
事件触发之前执行完毕,下文会详细介绍 laod 事件
DOMContentLoaded 与 load 的区别?
这是面试中的常考题,作为一名合格的前端开发人员,还是要认真学习一下的
当纯 HTML 被完全加载以及解析时,DOMContentLoaded 事件会被触发,而不必等待样式表,图片或者子框架完成加载
load 事件在整个页面及所有依赖资源如样式表和图片都已完成加载时触发。它与
DOMContentLoaded
不同,后者只要页面 DOM 加载完成就触发,无需等待依赖资源的加载 ——MDN
DOMContentLoaded
在前面的 defer
脚本中提到过,说到 defer
脚本会在 HTML
解析完成后、DOMContentLoaded
事件触发前执行。如果没有 defer 脚本,那么 HTML 解析完之后就会立即触发 DOMContentLoaded
事件,所以我们一般将 DOMContentLoaded
的触发时机当成是 DOM
树的构建完成时机;但这并不意味着 DOMContentLoaded
触发之后才会进行渲染树的生成,它和浏览器实际的渲染工作并没有什么关系,它只是 DOM
树构建完成之后触发的一个事件而已,在该事件触发之前,DOM
树就已经构建完成了,而且很有可能已经和样式规则合并成渲染树将图像绘制到页面中了,这也是为什么我们要将脚本放到文档底部的原因——可以加快首屏渲染速度
MDN
其实已经将 DOMContentLoaded
和 load
的区别讲的很明确了,因为 DOM
树的构建并不需要等待样式规则也就是 CSSOM
树构建完成,所以 DOMContentLoaded
触发的时候样式表可能还没有加载完成,不仅仅是样式表,图片、视频、子框架等也可能没加载好;但是 load
事件不一样,其被触发的时候不仅页面中所有的依赖资源包括图片、视频等都会被加载完成,而且通过上面有幅图片可知 async
脚本也会在 load
事件触发之前执行完毕
一句话总结一下,DOMContentLoaded
标志着 DOM
树构建完成,但页面中部分资源可能还没有加载完;load
标志着页面中所有的资源都已加载完成,包括图片、视频等资源
CSS 是否阻塞 HTML 的解析?
通过页面渲染的流程图我们可以看出 HTML
和 CSS
浏览器是可以并行解析的,也就是说外联 CSS
的加载和解析并不会阻塞 HTML
的解析,不过在 HTML5
标准中增加了一项规定,那就是浏览器在执行 script
脚本之前必须要确保该脚本之前的外联 CSS
已经加载解析完成,首先我们先想想这样做的目的是什么?
比如在 sript
脚本中我们可能会调用 getComputedStyle
等方法获取对应的 DOM
元素样式,如果在该脚本之前的外联 CSS
还没有被加载解析好,那么获取到的样式可能就是不准确的,所以在 HTML5
版本中才有了这个规定
这样一来,CSS
是否阻塞 HTML
解析这个问题可能就变得复杂了。如果某个外联 CSS
后面并没有任何 script
标签,那么还是像我们之前说的答案一样,该外联 CSS
的加载解析并不会阻塞 HTML
的渲染;但是如果该外联 CSS
后面有 script
标签,当解析到 script
标签时,js
文件虽然可以和 CSS
文件一起并行加载,但是 js
必须要等到 CSS
文件加载解析完成后才能执行,又因为 js
的执行在大部分情况下都会阻塞 HTML
的解析,相当于 CSS
通过阻塞 js
的执行来间接阻止了 HTML
的渲染
性能优化
基于浏览器渲染原理,我们可以有如下的优化手段:
-
JS
优化:script
标签加上defer
属性 和async
属性用于在不阻塞页面文档解析的前提下加载脚本defer
属性:用于开启新的线程下载脚本文件,并使脚本在HTML
文档解析完成后执行async
属性:HTML5
新增属性,用于异步下载脚本文件,下载完毕立即执行代码
使用场景:页面中有个动画使用 JS
做的,我们想要尽快展示这个动画但又不想阻塞 HTML
的解析,这时如果将对应的脚本单纯放到底部,虽然不会阻塞文档解析,但是需要等到文档解析完之后才进行加载执行,如果给脚本添加上了 defer
属性,那么脚本就可以在文档解析阶段并行加载,等到文档解析完之后就可以直接执行了,这样既不会阻塞文档加载,又加快了展示动画的速度
CSS
优化:link
标签的rel
属性值设置为preload
能够让你在提前加载当前页面中需要的资源,比如外联css
样式表,这样可以更快构建完CSSOM
树,加快首屏渲染速度
总结
本文讲解了一些有关浏览器渲染的细节,比如 js
和 css
到底会不会阻塞 HTML
文档的解析?HTML
文档解析完是否标志着 DOM
树构建完全,以及面试中常考的 DOMContentLoaded
与 load
的区别?如果这篇文章大家有什么问题都可以在评论区提出,希望看到这篇文章的读者能够有所收获~
转载自:https://juejin.cn/post/7145675831615389710