基于本文回答

播面 播面

刷题像听歌,多听自然懂
0
评论

为什么 JS 脚本的执行会阻塞页面的渲染?

知识点图片

JS 脚本执行之所以会阻塞页面渲染,核心原因在于浏览器渲染进程中的GUI 渲染线程JS 引擎线程互斥的(Mutually Exclusive)。

简单来说,浏览器为了保证页面内容的一致性,不允许一边画页面(渲染),一边改页面(JS 执行)。

以下是详细的深度解析:

1. 根本原因:线程互斥与“主线程”

在浏览器的每个渲染进程(Tab 页)中,有一个主线程(Main Thread)。这个主线程非常繁忙,它既要负责解析 HTML、计算样式、布局(Layout)、绘制(Paint),又要负责执行 JavaScript 代码。

  • GUI 渲染线程:负责构建 DOM 树、CSSOM 树、渲染树以及绘制页面。
  • JS 引擎线程:负责解析和执行 JavaScript 脚本。

这两个线程是互斥的。这意味着当 JS 引擎执行时,GUI 线程会被挂起(冻结),GUI 更新会被保存在一个队列中,等到 JS 引擎空闲时立即执行。反之亦然。

2. 为什么要设计成互斥?(为了数据一致性)

这是为了防止竞态条件(Race Condition)

JavaScript 的主要作用之一就是操作 DOM(文档对象模型)和 CSSOM(CSS 对象模型)。

  • JS 可以删除 DOM 节点。
  • JS 可以添加 DOM 节点。
  • JS 可以修改元素的样式(改变宽高等)。

试想一下,如果两者不互斥,并行执行会发生什么?
假设 GUI 线程正在渲染一个 <div>,准备把它画在屏幕上。与此同时,JS 线程正在执行代码,把这个 <div> 删除了,或者把它的背景色改了。
那么浏览器该听谁的?是画出旧的样式,还是画出新的?这会导致渲染结果不可预期,甚至导致渲染崩溃。

因此,浏览器采取了“同步阻塞”的策略:只要 JS 在执行,渲染就必须停下来,等待 JS 告诉它最终的 DOM 结构是什么样子的。

3. 阻塞的具体流程

当浏览器解析 HTML 文档时,流程如下:

  1. HTML 解析:浏览器从上到下解析 HTML,构建 DOM 树。
  2. 遇到 <script>:当解析器遇到 <script> 标签时,它不知道这段脚本会做什么(可能会调用 document.write() 重写整个页面,也可能修改全局样式)。
  3. 暂停渲染:为了保险起见,浏览器暂停 DOM 的构建和页面的渲染。
  4. 执行 JS:控制权移交给 JS 引擎。
    • 如果是外部脚本(src),还需要先下载(下载过程通常也会阻塞 HTML 解析,虽然现代浏览器有预加载扫描器优化下载,但执行依然是阻塞的)。
    • 下载完成后,立即执行 JS 代码。
  5. 恢复渲染:JS 执行完毕后,控制权交还给 GUI 线程,继续解析剩余的 HTML 并渲染。

4. 这种机制带来的问题

如果 JS 文件很大,或者 JS 执行时间过长(例如由于复杂的计算或死循环),就会导致:

  • 白屏时间变长:用户长时间看不到页面内容。
  • 页面卡顿(掉帧):如果页面已经渲染出来,但 JS 抢占了主线程,用户的滚动、点击等交互无法得到及时响应。

5. 解决方案(最佳实践)

为了避免 JS 阻塞关键渲染路径(Critical Rendering Path),开发者通常采用以下方法:

  1. 放在底部:将 <script> 标签放在 <body> 标签的闭合前。这样浏览器会先解析完所有的 HTML 元素并显示出来,最后再加载 JS。
  2. defer 属性<script defer src="...">
    • 推荐使用。脚本会并行下载,但会等到 HTML 解析完成后(DOMContentLoaded 事件触发前)才执行。且保证执行顺序。
  3. async 属性<script async src="...">
    • 脚本并行下载,下载完立即执行(此时依然会阻塞渲染)。不保证执行顺序,适用于统计代码等不依赖 DOM 的脚本。

总结

JS 阻塞渲染是因为JS 可以修改 DOM 和 CSS。为了确保渲染出的页面与 JS 修改后的状态一致,浏览器强制让 JS 执行UI 渲染在同一个主线程上互斥进行。

00:00
00:00