创见博客
浏览器如何解析 HTML:从下载文档到页面渲染
七崽爱吃小饼干2026/07/27阅读 4

当我们在地址栏输入一个 URL,或者打开一个本地 HTML 文件时,浏览器并不是等所有资源都下载完,再一次性把页面画出来。真实过程更像一条流水线:HTML 一边下载,一边解析;资源一边发现,一边请求;JavaScript 可能随时打断解析;CSS 会影响渲染时机;图片、字体、异步脚本则在不同阶段继续补齐页面。

理解这套流程,对前端性能、白屏优化、SEO、SSR、低代码页面导出都非常重要。

本文从浏览器解析 HTML 的顺序出发,解释它到底先执行什么、什么时候渲染、为什么普通 script 会阻塞页面,以及为什么 SEO 关键内容最好直接出现在初始 HTML 中。

一、浏览器不是“下载完 HTML 再解析”

浏览器拿到 HTML 响应后,会边接收字节流边解析。

简化过程如下:

text
下载 HTML 字节流
-> 解码成字符
-> 词法分析,生成 token
-> 根据 token 构建 DOM Tree
-> 发现外部资源并发请求
-> 构建 CSSOM
-> DOM + CSSOM 合成 Render Tree
-> Layout
-> Paint
-> Composite

也就是说,HTML 文档不需要完整下载完,浏览器就可以开始构建 DOM。

例如:

html
<!doctype html>
<html>
  <head>
    <title>Hello</title>
  </head>
  <body>
    <h1>Hello World</h1>
  </body>
</html>

浏览器解析到 <title> 时,就会把标题节点放入 DOM;解析到 <h1> 时,才会创建 body 里的标题节点。

所以一个很重要的结论是:当前脚本只能稳定访问已经解析过的 DOM,不能访问还没被解析到的后续 DOM。

二、HTML 解析是从上到下的

看这个例子:

html
<head>
  <script>
    console.log(document.querySelector('#app'))
  </script>
</head>
<body>
  <div id="app"></div>
</body>

输出结果是:

js
null

原因很简单:浏览器执行 head 中的 script 时,body 还没有被解析,<div id="app"> 还不存在于 DOM Tree 中。

如果改成:

html
<body>
  <div id="app"></div>
  <script>
    console.log(document.querySelector('#app'))
  </script>
</body>

这时就能找到节点,因为 <div id="app"> 已经在脚本之前被解析完成。

这也是为什么早期很多页面会把业务脚本放在 body 底部。

三、普通 script 会阻塞 HTML 解析

普通同步脚本是 HTML 解析流程里最关键的阻塞点。

html
<script src="main.js"></script>

当浏览器解析到这个标签时,会执行下面的流程:

text
暂停 HTML 解析
-> 下载 main.js
-> 执行 main.js
-> 继续解析后面的 HTML

为什么必须暂停?

因为 JavaScript 可以改变当前文档结构。

例如:

js
document.write('<div>inserted by script</div>')
document.body?.remove()

如果浏览器不暂停解析,而是继续往后构建 DOM,那么脚本一旦改写文档,就可能导致解析结果和脚本语义不一致。

所以浏览器选择保证语义正确:遇到同步 script,先停下来,把脚本执行完。

四、CSS 不阻塞 DOM 解析,但会阻塞渲染

CSS 的行为和 JavaScript 不一样。

html
<link rel="stylesheet" href="style.css">
<div>Hello</div>

浏览器遇到 CSS 文件时,会开始下载 CSS,但通常不会停止继续解析 HTML。

也就是说:

text
CSS 不阻塞 DOM 构建
CSS 阻塞页面渲染

原因是浏览器要把页面画出来,必须同时知道 DOM 结构和 CSS 样式。

只有 DOM Tree,没有 CSSOM,浏览器不知道:

text
元素是否 display: none
字体多大
颜色是什么
盒子宽高是多少
是否需要换行
是否创建新的层

所以 CSS 被称为 render-blocking resource,也就是渲染阻塞资源。

五、CSS 还会阻塞后续同步 JS 的执行

很多人容易忽略这个细节:CSS 不阻塞 DOM 解析,但会阻塞后续同步 JS 执行。

例如:

html
<link rel="stylesheet" href="style.css">
<script src="main.js"></script>

实际顺序通常是:

text
发现 style.css,开始下载
发现 main.js,暂停 HTML 解析,开始下载 JS
等待 style.css 下载并解析完成
执行 main.js
继续解析 HTML

为什么 JS 要等前面的 CSS?

因为 JS 可以读取样式:

js
const color = getComputedStyle(document.body).color

如果 CSS 还没加载完,JS 读到的样式就可能是错误的。

所以浏览器必须保证:当同步 JS 执行时,它前面已经发现的 CSS 对当前样式计算是可用的。

六、async script:下载完成就执行,顺序不保证

async 脚本不会阻塞 HTML 解析的下载阶段。

html
<script src="analytics.js" async></script>

它的行为是:

text
发现脚本
-> 立即开始下载
-> HTML 继续解析
-> 脚本下载完成
-> 立刻打断 HTML 解析并执行

如果有多个 async 脚本:

html
<script src="a.js" async></script>
<script src="b.js" async></script>

执行顺序不保证。谁先下载完成,谁先执行。

所以 async 适合:

text
埋点
广告
监控 SDK
第三方独立组件

不适合有顺序依赖的业务代码。

七、defer script:不阻塞解析,DOM 完成后按顺序执行

defer 更适合主业务脚本。

html
<script src="main.js" defer></script>

它的行为是:

text
发现脚本
-> 立即开始下载
-> 不阻塞 HTML 解析
-> 等 DOM 解析完成
-> 按 HTML 中出现的顺序执行
-> 触发 DOMContentLoaded

多个 defer 脚本会保持顺序:

html
<script src="a.js" defer></script>
<script src="b.js" defer></script>

即使 b.js 先下载完成,也必须等 a.js 先执行。

这使得 defer 很适合这种场景:

text
脚本需要访问完整 DOM
脚本之间存在顺序依赖
不希望阻塞首屏 HTML 解析

八、图片不会阻塞 HTML 解析

图片资源的加载方式更宽松。

html
<img src="hero.png" alt="hero">

浏览器解析到 img 标签时,会发起图片请求,但不会暂停 HTML 解析。

也就是说:

text
图片不阻塞 DOM 解析
图片不阻塞 JS 执行
图片通常不阻塞首次渲染
图片会影响视觉完整时间

如果图片没有声明宽高,图片下载完成后,浏览器可能重新计算布局,导致页面抖动。这就是 CLS 问题的常见来源。

所以更好的写法是:

html
<img src="hero.png" width="1200" height="600" alt="hero">

浏览器可以提前为图片预留空间,减少布局偏移。

九、字体也是延迟影响渲染体验的资源

字体通常通过 CSS 引入:

css
@font-face {
  font-family: 'BrandFont';
  src: url('/brand.woff2') format('woff2');
}

字体文件不一定在解析到 @font-face 时立刻下载。通常是当页面实际使用这个字体时,浏览器才会请求字体资源。

字体加载会影响文本显示,常见现象包括:

text
FOIT:文字短暂不可见
FOUT:先显示 fallback 字体,再切换成目标字体

可以通过 font-display 控制策略:

css
@font-face {
  font-family: 'BrandFont';
  src: url('/brand.woff2') format('woff2');
  font-display: swap;
}

十、浏览器什么时候开始渲染?

浏览器不是等 HTML 全部解析完才渲染。

只要满足一些条件,就可能开始首次绘制:

text
已经有可见 DOM
关键 CSS 已经可用
主线程有空闲时间
没有必须先执行的阻塞脚本

渲染主流程可以简化为:

text
DOM Tree
+ CSSOM
-> Render Tree
-> Layout
-> Paint
-> Composite

其中:

text
DOM Tree:页面结构
CSSOM:样式规则
Render Tree:真正需要显示的节点
Layout:计算位置和尺寸
Paint:绘制像素
Composite:图层合成

如果后续 JS 修改 DOM 或样式,浏览器可能重新执行其中一部分流程。

例如:

js
el.style.width = '200px'

可能触发:

text
Style Recalculation
-> Layout
-> Paint
-> Composite

而:

js
el.style.transform = 'translateX(100px)'

在很多情况下只需要 Composite,成本更低。

十一、DOMContentLoaded 和 load 的区别

前端经常遇到两个事件:DOMContentLoaded 和 load。

DOMContentLoaded 表示:

text
HTML 已经解析完成
DOM Tree 已经构建完成
defer 脚本已经执行完成
不等待图片、字体、iframe 全部加载完成

load 表示:

text
HTML、CSS、JS、图片、iframe 等主要资源都加载完成

所以通常顺序是:

text
HTML 解析
-> 同步脚本执行
-> DOM 构建完成
-> defer 脚本执行
-> DOMContentLoaded
-> 图片/字体/iframe 等资源继续完成
-> load

如果你只是要操作 DOM,通常用 DOMContentLoaded 就够了。

如果你要等图片尺寸、iframe、所有资源完成,再用 load。

十二、一个完整例子

看下面这个页面:

html
<!doctype html>
<html>
<head>
  <link rel="stylesheet" href="style.css">

  <script>
    console.log('inline head script')
  </script>

  <script src="sync.js"></script>
  <script src="defer.js" defer></script>
  <script src="async.js" async></script>
</head>
<body>
  <h1>Hello</h1>
  <img src="hero.png" alt="hero">

  <script>
    console.log('inline body script')
  </script>
</body>
</html>

大致流程是:

text
1. 开始下载并解析 HTML
2. 解析到 link,发现 style.css,开始下载 CSS
3. 遇到 head 内联 script
4. 等前面的 style.css,如果还没完成
5. 执行 head 内联 script
6. 遇到 sync.js,暂停 HTML 解析
7. 下载并执行 sync.js
8. 发现 defer.js,开始下载,不阻塞解析
9. 发现 async.js,开始下载,不阻塞解析
10. 解析 body
11. 创建 h1 DOM 节点
12. 发现 hero.png,开始下载图片
13. CSSOM 准备好后,浏览器可以进行首次渲染
14. 遇到 body 内联 script,暂停解析并执行
15. HTML 解析完成
16. 执行 defer.js
17. 触发 DOMContentLoaded
18. async.js 在下载完成后执行,时机不固定
19. 图片加载完成后更新绘制
20. 所有资源完成后触发 load

这里最需要注意的是:async.js 的执行点不稳定。它可能在 body 解析前执行,也可能在 DOMContentLoaded 后执行,取决于它什么时候下载完成。

十三、这套机制对 SEO 的影响

SEO 的关键原则是:重要内容尽量直接出现在初始 HTML 中。

传统爬虫主要看:

text
title
meta description
canonical
h1/h2
正文文本
链接结构
图片 alt
结构化数据

现代搜索引擎,例如 Googlebot,具备执行 JavaScript 的能力,但这不代表可以完全依赖 JS 渲染 SEO 内容。

原因包括:

text
JS 渲染可能进入延迟队列
接口请求可能失败
第三方资源可能超时
robots 策略可能阻止资源加载
执行成本更高,收录变慢

所以 SSR、SSG、服务端直出 HTML 对 SEO 更稳定。

如果一个页面的正文、标题、链接都已经在初始 HTML 中,即使爬虫不执行 JS,也能理解页面主题。

如果页面只有:

html
<div id="root"></div>
<script src="app.js"></script>

那么 SEO 内容就高度依赖 JS 执行结果,稳定性会差很多。

十四、实践建议

写 HTML 或优化页面性能时,可以记住这些规则:

text
关键 CSS 尽量小,避免阻塞首屏太久
业务脚本优先使用 defer
独立第三方脚本使用 async
不要把同步 script 放在 head 里阻塞首屏,除非确实必要
需要查询 body DOM 的脚本放在 body 底部,或使用 defer / DOMContentLoaded
首屏图片设置 width / height,减少布局偏移
SEO 关键内容直接输出到初始 HTML
字体使用 font-display,避免文字长时间不可见

总结

浏览器解析 HTML 的核心不是“读完再画”,而是“边解析、边下载、边执行、边渲染”。

可以用一句话概括:

text
HTML 构建 DOM,CSS 构建 CSSOM,JS 可能阻塞和修改一切,浏览器在保证语义正确的前提下尽量早地渲染页面。

再压缩成记忆版:

text
HTML:从上到下解析,构建 DOM
CSS:不阻塞 DOM,但阻塞渲染,也会阻塞后续同步 JS
普通 JS:阻塞 HTML 解析
async JS:下载完立即执行,顺序不保证
defer JS:DOM 完成后按顺序执行
图片:不阻塞解析,但影响视觉完成
DOMContentLoaded:DOM 和 defer 完成
load:页面主要资源完成

理解这套流程后,再看页面白屏、脚本执行时机、CSS 阻塞、SEO 直出、SSR 与 CSR 的差异,都会清晰很多。

评论
0/100