likes
comments
collection
share

CSS 和 JS 是否会阻塞 HTML 的解析?

作者站长头像
站长
· 阅读数 65

“我报名参加金石计划1期挑战——瓜分10万奖池,这是我的第3篇文章,点击查看活动详情

页面的渲染过程

  1. 在浏览器输入 URL 并按下回车后,服务端返回 HTML 文档
  1. 浏览器自顶向下解析 HTML 文档,解析的过程中会根据解析到的内容构建 DOM
  • 解析到 style 标签(外联样式)会并行加载对应的资源,加载完成后解析样式构建样式规则,生成 CSSOM 树(js 执行是单线程的,但浏览器并不是,所以其可以一边解析 HTML,一边解析 CSS
  • 解析到 script 标签(这里说的是最普通的,deferasync 属性后续会有详细介绍)时会停止 HTML 的解析并加载脚本,加载完成之后立即执行,随后 HTML 继续进行解析
  1. DOM 树和样式规则也就是 CSSOM 树构建完成之后会进行合并,生成渲染树 render tree
  1. 浏览器会对渲染树进行布局,目的是为了得到相关的布局信息,比如 DOM 元素在浏览器上的具体坐标和大小,最终生成布局树 layout tree
  1. 浏览器会对布局树进行绘制和分层,将布局树转换为对应的像素信息,最终传递给 GPU 将页面绘制出来呈现给用户

CSS 和 JS 是否会阻塞 HTML 的解析?

JS 是否阻塞 HTML 的解析?

上文中也提到了,对于普通的 script 标签来说,它的加载和执行都会阻塞 HTML 的解析,那如果添加上了 asyncdefer 属性会有什么变化呢?下面先看一张图片:

绿色代表 HTML 解析阶段

蓝色代表通过网络请求加载脚本阶段

红色代表执行脚本阶段

CSS 和 JS 是否会阻塞 HTML 的解析?

普通的 script 标签和上述说的一样,加载和执行都会阻塞 HTML 的解析,除非将普通的脚本放到 HTML 文档底部,这样就不会阻塞 HTML 的解析,首屏渲染的速度也会更快。但是 deferasync 这两个属性就不一样了,它们有个公共的地方就是对应的 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 事件

CSS 和 JS 是否会阻塞 HTML 的解析?

DOMContentLoaded 与 load 的区别?

这是面试中的常考题,作为一名合格的前端开发人员,还是要认真学习一下的

当纯 HTML 被完全加载以及解析时,DOMContentLoaded 事件会被触发,而不必等待样式表,图片或者子框架完成加载

load 事件在整个页面及所有依赖资源如样式表和图片都已完成加载时触发。它与 DOMContentLoaded 不同,后者只要页面 DOM 加载完成就触发,无需等待依赖资源的加载 ——MDN

DOMContentLoaded 在前面的 defer 脚本中提到过,说到 defer 脚本会在 HTML 解析完成后、DOMContentLoaded 事件触发前执行。如果没有 defer 脚本,那么 HTML 解析完之后就会立即触发 DOMContentLoaded 事件,所以我们一般将 DOMContentLoaded 的触发时机当成是 DOM 树的构建完成时机;但这并不意味着 DOMContentLoaded 触发之后才会进行渲染树的生成,它和浏览器实际的渲染工作并没有什么关系,它只是 DOM 树构建完成之后触发的一个事件而已,在该事件触发之前,DOM 树就已经构建完成了,而且很有可能已经和样式规则合并成渲染树将图像绘制到页面中了,这也是为什么我们要将脚本放到文档底部的原因——可以加快首屏渲染速度

MDN 其实已经将 DOMContentLoadedload 的区别讲的很明确了,因为 DOM 树的构建并不需要等待样式规则也就是 CSSOM 树构建完成,所以 DOMContentLoaded 触发的时候样式表可能还没有加载完成,不仅仅是样式表,图片、视频、子框架等也可能没加载好;但是 load 事件不一样,其被触发的时候不仅页面中所有的依赖资源包括图片、视频等都会被加载完成,而且通过上面有幅图片可知 async 脚本也会在 load 事件触发之前执行完毕

一句话总结一下,DOMContentLoaded 标志着 DOM 树构建完成,但页面中部分资源可能还没有加载完;load 标志着页面中所有的资源都已加载完成,包括图片、视频等资源

CSS 是否阻塞 HTML 的解析?

通过页面渲染的流程图我们可以看出 HTMLCSS 浏览器是可以并行解析的,也就是说外联 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 树构建完全,以及面试中常考的 DOMContentLoadedload 的区别?如果这篇文章大家有什么问题都可以在评论区提出,希望看到这篇文章的读者能够有所收获~

转载自:https://juejin.cn/post/7145675831615389710
评论
请登录