大家好,我是 前端架构师 - 大卫。
更多优质内容请关注微信公众号 @程序员大卫。
初心为助前端人🚀,进阶路上共星辰✨,
您的点赞与关注❤️,是我笔耕不辍的灯💡。
背景
我们都知道,现代浏览器会并行下载各种资源(如 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 树的解析
html
<!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>

打印结果:
csharp
null
说明:
bootstrap.bundle.js
加载时,浏览器会暂停 DOM 解析,还没解析到 <body>
的 <h1>
,所以 document.querySelector("h1")
得到的是 null
。这也是为什么 JS 一般建议放在页面底部的原因。
2. JS 的加载不会阻塞 DOM 树的渲染
html
<!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>

现象:
即使 JS 还在加载,DOM 已经被渲染出来了,页面的 "Hello" 文字已经显示为红色。
3. JS 的加载不会阻塞 CSS 的解析
html
<!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>

现象:
即使 JS 还没加载完,Hello
文字已经渲染到页面上了(黄色字体)。
二、验证 CSS 的加载阻塞行为
1. CSS 的加载不会阻塞 DOM 树的解析
html
<!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>

现象:
console.log
能成功拿到 h1
元素。说明 DOM 解析不会被 CSS 阻塞。
2. CSS 的加载会阻塞 DOM 树的渲染
说明: 虽然 DOM 结构已经被解析好了,但页面真正的"显示"会等 CSS 加载完才渲染。如果 CSS 很大或很慢,用户就会看到白屏,直到 CSS 下载完毕、解析完成,页面才正常显示(比如蓝色的 h1
才出现)。
3. CSS 的加载会阻塞 JS 的运行
html
<!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>

现象:
控制台打印的时间比较长(如 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 还在加载,浏览器会优先让已解析的内容尽快显示给用户,提升"首屏体验"。
- 设计目的 :JS 脚本经常需要读取和操作前面的 DOM 元素(比如绑定事件、修改内容),如果 DOM 还没解析好,JS 行为就会出错。因此浏览器选择在遇到
2. 关于 CSS 的加载阻塞
-
不阻塞 DOM 解析,但阻塞 DOM 渲染和后续 JS 的执行
- 设计目的:CSS 控制页面的外观和布局。浏览器在 CSS 未加载完成时,不会渲染对应的 DOM,是为了避免"无样式内容闪烁(FOUC)"和样式错乱。用户只会看到最终完整的页面样式,而不是先出现无样式内容再跳变成有样式内容。
- 阻塞 JS 执行 :CSS 的加载还会阻塞
<link>
后面 JS 的运行。这是因为很多 JS 依赖于元素的最终样式(如获取宽高、计算位置),如果 CSS 没应用完毕,这些 JS 操作就会出现错误或不准确。因此,只有等 CSS 加载完,后续的 JS 才被执行,保证脚本逻辑的安全和正确性。 - 并行优化:DOM 的解析依然是持续的(不会因为 CSS 加载而暂停),这样即便样式很大,DOM 结构可以先搭好,为后续渲染和交互做好准备。
综上,浏览器的这些加载阻塞行为,不是"性能问题",而是出于页面渲染一致性和脚本正确性的有意设计,既保障页面效果,又优化了加载体验。理解这些原理,才能在开发中写出结构更合理、加载更高效的页面。