HTML解析器采用流式处理,边接收字节边构建DOM树,依赖状态机进行词法分析,具备容错修复能力,但受脚本阻塞、编码声明缺失、标签误写等因素影响。

HTML解析器从字节流开始,不是等完整文件才动手
浏览器根本不会等整个HTML下载完再开始干活。你看到的“页面一点点加载出来”,本质就是解析器边收字节边建树。只要第一个<html>到达,解析就启动了——这和你在终端里用cat管道传内容给grep类似,是流式处理。
常见错误现象:本地双击打开index.html时页面空白或样式错乱,往往是因为解析中途遇到未闭合标签或编码声明缺失(比如没写<meta charset="utf-8">),导致后续token识别错位,DOM树构建异常。
- 必须在
<head>最开头声明<meta charset>,否则早期字节可能被误判为ASCII,中文变乱码 - 服务器返回的
Content-Type响应头带charset参数(如text/html; charset=utf-8)优先级高于HTML内声明 - 遇到
<script>且无async或defer时,解析会暂停,直到脚本下载、执行完毕——这是阻塞点,也是首屏延迟主因之一
词法分析阶段生成Token,状态机决定每个字符怎么解读
解析器内部是个状态机,遇到<就切到“标签开始”态,接着读字母就认为是开始标签,读/就转成结束标签态,读!则可能进入注释或DOCTYPE处理分支。这个过程不依赖正则,而是靠有限状态自动机逐字符推进。
容易踩的坑:手写HTML时用中文全角符号(如<、/)会被当成普通文本节点,无法触发标签逻辑;或者在属性值里漏掉引号,像<div class=header>,当后面紧跟<span>时,解析器可能把span误判为class的值。
立即学习“前端免费学习笔记(深入)”;
-
<!-- comment -->是合法Token,但会被完全忽略,不进DOM树 - 自闭合标签如
<img>、<br>在HTML5中不需要/>,加了也不报错,但<div/>这种写法会被当作<div></div>处理 - 遇到未知标签(如
<my-component>),浏览器照常建元素节点,只是没默认样式和语义
语法分析用栈构建DOM树,容错机制比你想象得更激进
DOM树不是严格按源码结构硬套出来的。浏览器会主动修复明显错误,比如<div><p></div>会被重排为<div><p></p></div>,因为解析器栈顶是<div>,遇到</div>前先强制闭合未完成的<p>。
这种修复虽让页面“看起来正常”,但会改变节点层级,影响CSS选择器匹配和JS查询结果。比如你写document.querySelector('div p'),原意是选<div><p>,但若实际DOM被修复成<div></div><p>,就查不到。
- 所有文本节点默认合并相邻空白符(换行、制表、多个空格→单个空格),除非父元素设了
white-space: pre -
<table>相关标签有特殊插入规则:没写<tbody>时,浏览器自动补上,所以document.querySelector('table > tbody')永远不为空 - 解析过程中遇到
<script>内联代码,会立即执行,此时DOM树只建到该<script>位置,document.body可能还是null
解析与渲染不是串行流水线,而是多线程协同的“竞速游戏”
HTML解析、CSS下载、图片解码、JS编译这些事,在现代浏览器里由不同线程并行推进。但关键约束在于:渲染树(Render Tree)必须等DOM树 + CSSOM树都就绪才能生成,而CSSOM又依赖所有blocking样式表下载完成。
所以哪怕HTML解析飞快,一个阻塞的main.css没回来,页面就卡在白屏——这不是解析慢,是渲染被挂起。这也是为什么rel="preload"对关键CSS有效,而rel="prefetch"无效。
-
<link rel="stylesheet">默认阻塞解析,但加上media="print"或media="(min-width: 900px)"且当前不匹配,就会异步加载,不阻塞 -
<script type="module">天然具有defer行为,即使没写属性,也会延迟到DOM构建完成后执行 - 使用
document.write()会清空当前文档并重建,彻底打断增量解析,现在已被大多数现代浏览器禁用或限制
解析真正复杂的地方不在语法层面,而在它和网络、样式、脚本三者的耦合深度——你改一行HTML,可能触发DNS查询、TLS握手、CSS重排、JS重编译,最终效果取决于这整条链路上最慢的那个环节。



















