JS 是否阻塞 HTML 的解析

256 阅读8分钟

页面的渲染过程

  1. 在浏览器输入 URL 并按下回车后,服务端返回 HTML 文档
  2. 浏览器自顶向下解析 HTML 文档,解析的过程中会根据解析到的内容构建 DOM 树

    解析到 style 标签(外联样式)会并行加载对应的资源,加载完成后解析样式构建样式规则,生成 CSSOM 树(js 执行是单线程的,但浏览器并不是,所以其可以一边解析 HTML,一边解析 CSS) 解析到 script 标签(这里说的是最普通的,defer 和 async 属性后续会有详细介绍)时会停止 HTML 的解析并加载脚本,加载完成之后立即执行,随后 HTML 继续进行解析

  3. DOM 树和样式规则也就是 CSSOM 树构建完成之后会进行合并,生成渲染树 render tree
  4. 浏览器会对渲染树进行布局,目的是为了得到相关的布局信息,比如 DOM 元素在浏览器上的具体坐标和大小,最终生成布局树 layout tree
  5. 浏览器会对布局树进行绘制和分层,将布局树转换为对应的像素信息,最终传递给 GPU 将页面绘制出来呈现给用户

JS 是否阻塞 HTML 的解析?

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

绿色代表 HTML 解析阶段 蓝色代表通过网络请求加载脚本阶段 红色代表执行脚本阶段

"图片自定义高度" height="" width=""

  1. 普通的 script 标签和上述说的一样,加载和执行都会阻塞 HTML 的解析,除非将普通的脚本放到 HTML 文档底部,这样就不会阻塞 HTML 的解析,首屏渲染的速度也会更快。
  2. defer、async 这两个属性就不一样了,它们有个公共的地方就是对应的 js 文件在加载时并不会阻塞 HTML 的解析
  3. 含有 defer 属性的脚本在加载完之后不会立即执行,而是会放到一个队列中,等到 HTML 解析完成之后依次取出来执行,所以 defer 属性对应的脚本有个明显的特点就是脚本的执行顺序和出现在 HTML 文档中的顺序一致,而且它一定会在 DOMContentLoaded 事件触发之前执行

这个布尔属性被设定用来通知浏览器该脚本将在文档完成解析后,触发 DOMContentLoaded (en-US) 事件前执行。 有 defer 属性的脚本会阻止 DOMContentLoaded 事件,直到脚本被加载并且解析完成。——MDN

  1. 含有 async 属性的脚本执行时机就没有 defer 要求那么严格,它的执行时机并不确定

可能是在 HTML 解析完成前,此时会立即停止 HTML 的解析并执行脚本 可能是在 DOMContentLoaded 事件触发后,此时 HTML 已经解析完成了, 这种情况是有可能发生的,因为脚本什么时候加载好并不是确定的. 这种情况下脚本的执行就没有阻塞 HTML 的解析

所以说脚本执行一定会延迟 DOMContentLoaded 事件的触发时间是不严谨的,但是 async 脚本一定会在 load 事件触发之前执行完毕

DOMContentLoaded 与 load 的区别?

当纯 HTML 被完全加载以及解析时,DOMContentLoaded 事件会被触发,而不必等待样式表,图片或者子框架完成加载。 load 事件在整个页面及所有依赖资源如样式表和图片都已完成加载时触发。它与 DOMContentLoaded 不同,后者只要页面 DOM 加载完成就触发,无需等待依赖资源的加载

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 优化

defer 属性:用于开启新的线程下载脚本文件,并使脚本在 HTML 文档解析完成后执行 async 属性:HTML5 新增属性,用于异步下载脚本文件,下载完毕立即执行代码

  • CSS 优化

link 标签的 rel 属性值设置为 preload 能够让你在提前加载当前页面中需要的资源,比如外联 css 样式表,这样可以更快构建完 CSSOM 树,加快首屏渲染速度

  • 优化 JavaScript 执行

window.requestAnimationFrame window.requestIdleCallback Web Worker

  • 回流重绘

回流和重绘会不断触发,这是不可避免的。但是,它们非常消耗资源,是导致网页性能低下的根本原因。

提高网页性能,就是要降低回流和重绘的频率和成本,尽可能少的触发重新渲染。

浏览器面对集中的 DOM 操作时会有一个优化策略:创建一个变化的队列,然后一次执行,最终只渲染一次。

div2.style.height = "100px";
div2.style.width = "100px";

上面的代码在浏览器优化后只会执行一次渲染。但是,如果代码写得不好变化的队列就会立即刷新,并进行渲染;这通常是在修改 DOM 之后,立即获取样式信息的时候。下面的样式信息会触发重新渲染:

1. offsetTop/offsetLeft/offsetWidth/offsetHeight
2. scrollTop/scrollLeft/scrollWidth/scrollHeight
3. clientTop/clientLeft/clientWidth/clientHeight
4. getComputedStyle()
  • 防抖和节流

在进行改变窗口大小、滚动网页、输入内容这些操作时,事件回调会十分频繁的被触发,严重增加了浏览器的负担,导致用户体验非常糟糕。此时,我们就可以考虑采用防抖和节流函数来处理这类调动频繁的事件回调,同时它们也不会影响实际的交互效果。