HTML 文档的渲染是一个多阶段协同的过程,核心分为「解析 → 布局 → 绘制」三大步骤;而 CSS 和 JS 文件的下载、执行,会通过不同方式影响渲染流程 —— 关键结论先明确:
- CSS 文件:下载不阻塞 HTML 解析,但阻塞 DOM 渲染(布局 + 绘制),且阻塞后续 JS 执行;
- JS 文件:下载和执行都会阻塞 HTML 解析(默认),也会阻塞 DOM 渲染;
- 可通过
async/defer关键字改变 JS 的阻塞行为。
一、先搞懂:HTML 文档完整渲染流程
浏览器加载 HTML 后,会按以下顺序执行流程(简化核心步骤):
-
HTML 解析(Parse HTML) :浏览器从顶部开始逐行解析 HTML 文本,构建 DOM 树(Document Object Model) (描述页面结构的节点树);解析过程中遇到
<link rel="stylesheet">(CSS)、<script>(JS)、<img>(图片)等资源,会触发资源下载。 -
CSS 解析(Parse CSS) :下载完成的 CSS 文件会被解析,构建 CSSOM 树(CSS Object Model) (描述样式规则的树);只有 DOM 树和 CSSOM 树都构建完成,才会进入下一步。
-
布局(Layout/Reflow) :结合 DOM 树和 CSSOM 树,计算每个 DOM 元素的位置、大小、间距等几何信息,生成「渲染树(Render Tree)」(仅包含可见元素,如
display: none的元素不会进入渲染树)。 -
绘制(Paint) :根据渲染树,浏览器将元素的样式(颜色、背景、边框等)绘制到屏幕像素上,最终呈现可视化页面。
-
回流(Reflow)/ 重绘(Repaint) (后续可能触发):
- 回流:元素几何信息变化(如宽高、位置),需重新执行「布局」步骤;
- 重绘:元素样式变化(如颜色、背景),无需重新布局,直接重新「绘制」;回流开销远大于重绘。
二、CSS 文件:下载不阻塞解析,阻塞渲染 + JS 执行
1. 核心行为:
- 下载不阻塞 HTML 解析:浏览器解析 HTML 时,遇到
<link rel="stylesheet">会并行下载 CSS 文件,同时继续解析后续 HTML(不会暂停解析)—— 因为 CSS 不影响 DOM 结构,提前解析 HTML 能提升效率。 - 阻塞 DOM 渲染(布局 + 绘制) :渲染树需要 DOM 树 + CSSOM 树共同构建,缺少 CSSOM 树时,浏览器无法计算元素样式和位置,会暂停渲染流程,直到 CSS 下载并解析完成。→ 现象:如果 CSS 下载缓慢,页面会先显示 “空白”,直到 CSS 加载完成后才会渲染出样式。
- 阻塞后续 JS 执行:JS 代码可能会操作 CSS 样式(如
document.styleSheets),为了避免 JS 读取到 “未完整解析的 CSS 样式”,浏览器会让 JS 等待前面的 CSS 下载 + 解析完成后,再执行 JS。→ 顺序依赖:<link rel="stylesheet">后面的<script>会被阻塞,直到 CSS 就绪。
2. 示例验证:
<!-- 解析 HTML 时,并行下载 style.css,同时继续解析后续 HTML -->
<link rel="stylesheet" href="style.css">
<!-- 这里的 JS 会等待 style.css 下载+解析完成后,才执行 -->
<script src="app.js"></script>
<!-- HTML 解析不会被 CSS 下载阻塞,会正常解析 div 节点 -->
<div class="box">Hello</div>
- 现象:
div会被正常解析到 DOM 树,但不会立即渲染(无样式 + 阻塞),直到style.css加载完成,才会结合 CSSOM 树进行布局和绘制。
3. 优化建议:
- 优先加载关键 CSS(如首屏样式内联到
<style>标签,避免外部 CSS 阻塞); - 非关键 CSS 延迟加载(如通过
media="print"标记,下载时不阻塞渲染,后续切换媒体类型); - 压缩 CSS 体积,使用 CDN 加速下载。
三、JS 文件:默认下载 + 执行都阻塞,可通过 async/defer 解除
JS 是「可修改 DOM/CSS」的脚本,浏览器无法预知 JS 会做什么(如 document.write 修改 DOM、document.style 修改 CSS),因此默认会严格阻塞流程,核心行为分「默认情况」和「async/defer 情况」:
1. 默认情况(无 async/defer):
- 下载阻塞 HTML 解析:浏览器解析 HTML 时,遇到
<script>会暂停 HTML 解析,先下载 JS 文件(单线程下载,早期浏览器不支持并行,现代浏览器支持有限并行,但解析仍会暂停)。 - 执行阻塞 DOM 渲染:JS 下载完成后,会立即执行(单线程执行),执行期间会阻塞渲染流程(布局 + 绘制)—— 因为 JS 可能修改 DOM(如
appendChild)或 CSSOM(如document.body.style.color = 'red'),浏览器需等待 JS 执行完成后,再重新同步 DOM/CSSOM 树。 - 顺序依赖:JS 会按「在 HTML 中出现的顺序」下载 + 执行,后续 JS 需等待前面 JS 完成。
2. async 关键字:异步下载,执行不阻塞解析但阻塞渲染
- 下载不阻塞 HTML 解析:JS 并行下载,HTML 解析继续(同 CSS 下载行为)。
- 执行不阻塞 HTML 解析,但阻塞渲染:JS 下载完成后,会暂停当前 HTML 解析和渲染,立即执行 JS;执行完成后,恢复解析和渲染。
- 顺序不保证:多个
asyncJS 的执行顺序由下载速度决定(谁先下载完谁先执行),不遵循 HTML 中的顺序。
3. defer 关键字:异步下载,延迟执行(不阻塞解析,可能阻塞渲染)
- 下载不阻塞 HTML 解析:同
async,JS 并行下载,HTML 解析继续。 - 执行不阻塞 HTML 解析,但可能阻塞渲染:JS 下载完成后,不会立即执行,而是等待整个 HTML 解析完成后(DOM 树构建完毕),再按「HTML 中出现的顺序」执行;执行时仍会阻塞渲染(因为 JS 可能修改 DOM/CSSOM),但此时 DOM 已完整,适合需要操作 DOM 的脚本。
4. 三者对比表:
| 特性 | 默认(无关键字) | async | defer |
|---|---|---|---|
| 下载是否阻塞解析 | 是 | 否(并行下载) | 否(并行下载) |
| 执行是否阻塞解析 | 是 | 是(下载完立即执行) | 否(HTML 解析完执行) |
| 执行是否阻塞渲染 | 是 | 是 | 是 |
| 执行顺序 | 按 HTML 顺序 | 按下载速度(无序) | 按 HTML 顺序 |
| 适合场景 | 需同步执行的脚本 | 独立脚本(如统计、广告) | 依赖 DOM 的脚本(如初始化组件) |
5. 示例验证:
<!-- 默认 JS:解析到这里暂停 HTML 解析,下载 app.js 并执行,再继续解析 -->
<script src="app.js"></script>
<!-- async JS:并行下载,下载完立即执行(暂停当前解析/渲染),顺序不确定 -->
<script src="stat.js" async></script>
<!-- defer JS:并行下载,HTML 解析完后按顺序执行 -->
<script src="init.js" defer></script>
- 现象:
app.js会阻塞 HTML 解析,导致后续 DOM 节点延迟解析;stat.js和init.js不会阻塞解析,但执行时仍会短暂阻塞渲染。
6. 优化建议:
- 脚本放在
<body>底部(默认情况):让 HTML 先解析完成,减少阻塞时间; - 依赖 DOM 的脚本用
defer(保证 DOM 完整,且按顺序执行); - 独立无依赖的脚本用
async(如统计、第三方 SDK,不影响主流程); - 避免同步加载大型 JS(会长时间阻塞解析和渲染);
- 内联关键 JS(如首屏初始化逻辑,无需下载,减少阻塞)。
四、补充:图片等资源的下载与阻塞
- 图片(
<img>)、视频(<video>)等媒体资源:下载不阻塞 HTML 解析,也不阻塞渲染; - 但图片下载完成后,可能触发「回流」(如图片宽高未指定,下载后改变元素大小),导致页面重新布局和绘制 —— 因此建议给图片设置固定宽高或
aspect-ratio,避免回流。
五、核心结论总结
-
HTML 渲染流程:解析 HTML 建 DOM → 解析 CSS 建 CSSOM → 结合生成渲染树 → 布局 → 绘制;
-
CSS 文件:
- 下载不阻塞 HTML 解析;
- 阻塞渲染(需等待 CSSOM 构建);
- 阻塞后续 JS 执行;
-
JS 文件:
- 默认:下载 + 执行都阻塞 HTML 解析,执行阻塞渲染;
async:下载不阻塞解析,执行阻塞解析 + 渲染,顺序无序;defer:下载不阻塞解析,执行不阻塞解析(HTML 解析完执行),执行阻塞渲染,顺序有序;
-
优化核心:减少关键资源阻塞时间,让 DOM 和 CSSOM 尽快就绪,避免不必要的回流重绘。