4. HTML解析与DOM构建
好,咱们今天聊聊渲染进程里最基础的一步——HTML解析。
很多人觉得这步很简单,不就是读标签吗?其实不然。我见过不少性能问题,根子就出在解析阶段。你想想看,浏览器拿到的是纯文本,要变成内存里一棵结构化的树,这中间的门道可不少。
4.1 HTML解析器:它不是正则匹配器
先纠正一个常见误区。有人以为HTML解析就是拿正则表达式去匹配标签。嗯,千万别这么干。HTML的容错性极强,语法又松散,正则根本搞不定。
真正的HTML解析器,是一个状态机。它逐字节读取数据,根据当前状态决定下一步动作。我在Chromium源码里看过,那个解析器有80多个状态,处理各种奇葩写法。
核心要点:HTML解析器是增量式的。它不需要等整个文档下载完才开始解析。边下载边解析,边解析边构建DOM树。这也是为什么浏览器能“流式”渲染页面。
4.2 词法分析:把字符流变成Token流
解析的第一步,是词法分析。说白了,就是把一堆字符拆成有意义的“词”。在HTML里,这些“词”就是Token。
Token有几种类型:
- 开始标签:比如
<div> - 结束标签:比如
</div> - 自闭合标签:比如
<br /> - 注释:比如
<!-- 注释 --> - 文本内容:标签之间的纯文字
- DOCTYPE:文档类型声明
词法分析器怎么工作?它维护一个输入流指针,不断往前扫描。每遇到一个 < 字符,就进入“标签开始”状态。然后继续读,直到遇到 > 才结束。
举个例子,对于这段HTML:
<div class="container">
Hello World
</div>
词法分析器会吐出这些Token:
StartTag: div(附带属性class="container")Character: "Hello World"EndTag: div
我的经验:有一次排查页面白屏问题,发现是词法分析阶段卡住了。原因是一个标签属性值里包含了未转义的 > 字符。解析器以为标签结束了,结果后面全乱了。所以,属性值里的特殊字符一定要转义,这不是小事。
4.3 语法分析:从Token到DOM树
词法分析完了,得到一堆Token。接下来语法分析器登场,它要把这些Token组织成一棵树——DOM树。
语法分析的核心是栈结构。解析器维护一个“开放元素栈”,遇到开始标签就入栈,遇到结束标签就出栈。栈顶元素永远是当前正在解析的元素。
过程大致是这样:
- 遇到
StartTag: html,创建html节点,入栈。 - 遇到
StartTag: head,创建head节点,入栈。此时head成为html的子节点。 - 遇到
EndTag: head,head出栈。栈顶回到html。 - 遇到
StartTag: body,创建body节点,入栈。 - 遇到
Character: "Hello",创建文本节点,挂到body下。 - 遇到
EndTag: body,body出栈。 - 遇到
EndTag: html,html出栈。解析完成。
最终生成的DOM树:
Document
└── html
├── head
└── body
└── "Hello"
注意:HTML的语法分析有容错机制。比如你忘了写 </p>,浏览器不会报错,它会自动帮你补上。但千万别依赖这个。我曾经遇到一个线上bug,就是因为某个页面少写了一个结束标签,Chrome和Safari的容错处理方式不同,导致布局错乱。嗯,血的教训。
4.4 特殊标签的处理:script与css
解析过程中,有两个特殊角色会影响流程:
| 标签类型 | 行为 | 影响 |
|---|---|---|
<script>(无async/defer) |
遇到后暂停HTML解析,下载并执行JS | 阻塞DOM构建 |
<script async> |
异步下载,下载完成后立即执行 | 可能阻塞,执行顺序不确定 |
<script defer> |
异步下载,HTML解析完成后执行 | 不阻塞 |
<link rel="stylesheet"> |
下载CSS,不阻塞解析 | 但会阻塞后续脚本的执行 |
这里有个细节很多人不知道。CSS虽然不阻塞HTML解析,但它会阻塞脚本执行。为什么?因为脚本可能读取样式信息(比如 getComputedStyle),如果CSS没加载完,脚本拿到的值就是错的。所以浏览器做了这个权衡。
建议:把 <script> 放在 </body> 之前,或者使用 defer 属性。把CSS放在 <head> 里尽早加载。这是最基础的性能优化,但很多人就是记不住。
4.5 DOM树构建完成之后
当解析器处理完所有Token,开放元素栈清空,DOM树就构建完成了。此时会触发 DOMContentLoaded 事件。
注意,DOMContentLoaded 和 load 事件不同:
DOMContentLoaded:DOM树构建完成,但图片、样式表等资源可能还没加载完。load:所有资源都加载完成了。
我个人习惯把初始化逻辑放在 DOMContentLoaded 里,而不是 load。这样用户能更快看到可交互的页面。
避坑指南:我曾经在 DOMContentLoaded 里操作了图片的尺寸,结果发现图片还没加载完,拿到的宽高是0。后来才意识到,图片的尺寸要在 load 事件里获取,或者用 ResizeObserver 监听。嗯,细节决定成败。
4.6 总结一下
HTML解析与DOM构建,说白了就是三步:
- 词法分析:字符流 → Token流
- 语法分析:Token流 → DOM树(用栈结构)
- 特殊处理:处理
script和style的阻塞逻辑
整个过程是增量的、流式的。浏览器不会傻等,它一边下载一边解析一边渲染。理解了这个机制,你就能解释很多现象:为什么 script 放头部会白屏?为什么CSS要提前加载?为什么 DOMContentLoaded 比 load 早?
嗯,这些问题的答案,都在这一章里了。