页面加载太慢?一文搞懂 JS 和 CSS 阻塞机制!

背景

我们都知道,现代浏览器会并行下载各种资源(如 JS、CSS、图片等),但 JS 和 CSS 的加载与阻塞行为到底是什么?本文将通过实际案例、实验、配图,一次性讲清楚这些常见但又容易混淆的知识点。

先说结论

1. JS 的加载

  • 会阻塞 DOM 树的解析 (也就是说,只要 <script> 没有执行完,HTML 后面的内容不会被解析成 DOM)
  • 不会阻塞 DOM 树的渲染 (如果 DOM 已经解析出来,就能渲染,不用等 JS 下载完)
  • 不会阻塞 CSS 的解析 (浏览器会并行下载、解析 CSS 文件)

2. CSS 的加载

  • 不会阻塞 DOM 树的解析 (HTML 内容依然会被解析成 DOM)
  • 会阻塞 DOM 树的渲染 (页面直到 CSS 下载并解析完成后才会“真正”渲染出来,否则会出现白屏
  • 会阻塞 JS 的运行 (在 <link> 后面的 JS 脚本,会等 CSS 下载完后再执行)
可以发现:CSS 和 JS 的“阻塞行为”刚好互补,相反。

准备工作

我们准备了一个稍大的 JS 文件和一个稍大的 CSS 文件。 在 Chrome DevTools 的 Network 面板,调整网速为 3G,勾选 Disable cache,以便模拟慢速网络环境,更容易观察资源的加载顺序与阻塞行为。

一、验证 JS 的加载阻塞行为

1. JS 的加载会阻塞 DOM 树的解析

<!DOCTYPE html>
<html>
 <head>
  <title>JS 阻塞</title>
  <meta charset="UTF-8" />
  <script>
   setTimeout(() => {
    console.log(document.querySelector("h1"));
   }, 1000);
  </script>
  <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.7/dist/js/bootstrap.bundle.js"></script>
 </head>
 <body>
  <h1>Hello</h1>
 </body>
</html>
image

打印结果:

null

说明:

bootstrap.bundle.js 加载时,浏览器会暂停 DOM 解析,还没解析到 <body> 的 <h1>,所以 document.querySelector("h1") 得到的是 null。这也是为什么 JS 一般建议放在页面底部的原因。

2. JS 的加载不会阻塞 DOM 树的渲染

<!DOCTYPE html>
<html>
 <head>
  <title>JS 阻塞</title>
  <meta charset="UTF-8" />
 </head>
 <body>
  <h1 style="color: red">Hello</h1>
  <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.7/dist/js/bootstrap.bundle.js"></script>
 </body>
</html>
image

现象:

即使 JS 还在加载,DOM 已经被渲染出来了,页面的 “Hello” 文字已经显示为红色。

3. JS 的加载不会阻塞 CSS 的解析

<!DOCTYPE html>
<html>
 <head>
  <title>JS 阻塞</title>
  <meta charset="UTF-8" />
  <link
   href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.7/dist/css/bootstrap.min.css"
   rel="stylesheet"
  />
 </head>
 <body>
  <h1 class="text-warning">Hello</h1>
  <script src="./js/big.js"></script>
 </body>
</html>
image

现象:

即使 JS 还没加载完,Hello 文字已经渲染到页面上了(黄色字体)。

二、验证 CSS 的加载阻塞行为

1. CSS 的加载不会阻塞 DOM 树的解析

<!DOCTYPE html>
<html>
 <head>
  <title>CSS </title>
  <meta charset="UTF-8" />
  <script>
   setTimeout(() => {
    console.log(document.querySelector("h1"));
   }, 100);
  </script>
  <link
   href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.7/dist/css/bootstrap.css"
   rel="stylesheet"
  />
 </head>
 <body>
  <h1 class="text-primary">Hello</h1>
 </body>
</html>
image

现象:

console.log 能成功拿到 h1 元素。说明 DOM 解析不会被 CSS 阻塞

2. CSS 的加载会阻塞 DOM 树的渲染

说明: 虽然 DOM 结构已经被解析好了,但页面真正的“显示”会等 CSS 加载完才渲染。如果 CSS 很大或很慢,用户就会看到白屏,直到 CSS 下载完毕、解析完成,页面才正常显示(比如蓝色的 h1 才出现)。

3. CSS 的加载会阻塞 JS 的运行

<!DOCTYPE html>
<html>
 <head>
  <title>CSS</title>
  <meta charset="UTF-8" />
  <script>
   console.time("CSS load");
  </script>
  <link
   href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.7/dist/css/bootstrap.css"
   rel="stylesheet"
  />
  <script>
   console.timeEnd("CSS load");
  </script>
 </head>
 <body>
  <h1 class="text-primary">Hello</h1>
 </body>
</html>
image

现象:

控制台打印的时间比较长(如 2759ms),JS被阻塞,必须等 CSS 完全加载解析后才会执行。

当然可以,下面这个总结会结合你列出的所有具体结论,并且明确对应到浏览器的设计行为与原理,让读者能从“为什么这样设计”更深入理解这些加载阻塞现象。

总结:浏览器资源加载阻塞机制背后的设计原理

浏览器对 JS 和 CSS 资源的加载阻塞行为,其实是围绕着页面渲染的正确性、交互的安全性和用户体验三者进行权衡与优化,核心原理体现在以下几点:

1. 关于 JS 的加载阻塞

  • 阻塞 DOM 树的解析,但不阻塞 DOM 渲染,也不阻塞 CSS 的解析
    • 设计目的:JS 脚本经常需要读取和操作前面的 DOM 元素(比如绑定事件、修改内容),如果 DOM 还没解析好,JS 行为就会出错。因此浏览器选择在遇到 <script> 时暂停 DOM 解析,直到该 JS 加载并执行完毕再继续解析剩余的 HTML。这样可以确保脚本拿到的是完整、有效的 DOM。
    • 并行优化:JS 的加载和 CSS 的解析是并行的,这样不会让 CSS 的加载因为 JS 阻塞而延迟,从而加快页面样式的生效速度,提高整体加载效率。
    • 渲染保障:DOM 树一旦被解析出来就会渲染,即便 JS 还在加载,浏览器会优先让已解析的内容尽快显示给用户,提升“首屏体验”。

2. 关于 CSS 的加载阻塞

  • 不阻塞 DOM 解析,但阻塞 DOM 渲染和后续 JS 的执行
    • 设计目的:CSS 控制页面的外观和布局。浏览器在 CSS 未加载完成时,不会渲染对应的 DOM,是为了避免“无样式内容闪烁FOUC)”和样式错乱。用户只会看到最终完整的页面样式,而不是先出现无样式内容再跳变成有样式内容。
    • 阻塞 JS 执行:CSS 的加载还会阻塞 <link> 后面 JS 的运行。这是因为很多 JS 依赖于元素的最终样式(如获取宽高、计算位置),如果 CSS 没应用完毕,这些 JS 操作就会出现错误或不准确。因此,只有等 CSS 加载完,后续的 JS 才被执行,保证脚本逻辑的安全和正确性。
    • 并行优化:DOM 的解析依然是持续的(不会因为 CSS 加载而暂停),这样即便样式很大,DOM 结构可以先搭好,为后续渲染和交互做好准备。

综上,浏览器的这些加载阻塞行为,不是“性能问题”,而是出于页面渲染一致性和脚本正确性的有意设计,既保障页面效果,又优化了加载体验。理解这些原理,才能在开发中写出结构更合理、加载更高效的页面。

posted @ 2025-10-09 09:12  蓦然JL  阅读(17)  评论(0)    收藏  举报
访问主页
关注我
关注微博
私信我
返回顶部