DOM树:JavaScript是如何影响DOM树构建的|浏览器篇
# DOM树:JavaScript是如何影响DOM树构建的
30 秒速记
DOM是渲染流水线的关键中间结构,后续多个阶段都会直接或间接依赖它。- 分析页面构建时,应沿网络字节流进入渲染引擎的路径,关注数据如何逐步形成
DOM树。 - 解析器遇到
JavaScript后需要协调脚本执行与文档解析,因为脚本可能改变已经生成的节点。 - 跨站资源会让解析过程涉及额外的资源获取与处理,不能只看本地
HTML的结构转换。
DOM 是渲染流程的基础结构,而 JavaScript 会通过暂停解析或修改节点,直接影响它的构建。 浏览器会把网络字节流逐步转成字符和令牌,再边下载边生成树,并按 HTML 规则补全或修正部分结构。遇到普通脚本时,解析器要先等脚本执行完,因为脚本可能通过 document.write 或节点操作改变当前文档。跨站资源不会改变树构建算法,但网络延迟、同源策略、CORS 和内容安全策略可能影响资源能否及时使用。
在上一篇文章中,我们通过开发者工具中的网络面板,介绍了网络请求过程的几种性能指标以及对页面加载的影响。
而在渲染流水线中,后面的步骤都直接或者间接地依赖于 DOM 结构,所以本文我们就继续沿着网络数据流路径来介绍 DOM 树是怎么生成的。然后再基于 DOM 树的解析流程介绍两块内容:第一个是在解析过程中遇到 JavaScript 脚本,DOM 解析器是如何处理的?第二个是 DOM 解析器是如何处理跨站点资源的?
原理拆解: 网络层交付的是字节流,渲染引擎先依据编码把它转换为字符,再由 HTML 解析器进行词法处理、生成令牌,并按照树构建规则创建和挂接节点。这个过程通常是流式的,不必等完整文档下载完才开始,所以下载与解析能够交叠进行。生成的 DOM 不只是元素标签的镜像:浏览器还会补全某些隐含结构,并按照 HTML 解析规则纠正部分不规范嵌套。
具体例子: 解析器遇到没有 async、defer 的外部经典脚本时,会暂停后续文档解析,等待脚本取得并立即执行,因为脚本可能通过 document.write、节点插入或删除改变当前文档。脚本执行时只能可靠访问已经解析完成的前置节点;位于其后的元素尚未进入 DOM。带 defer 的经典脚本可在文档解析完成后、DOMContentLoaded 前按文档顺序执行,从而减少对树构建的阻塞。
边界与排查: 跨站脚本、图片或接口并不会因为来源不同就改变 DOM 的基本树构建算法,但会受到网络延迟、同源策略、CORS、内容安全策略等约束。图片下载通常不阻塞 DOM 构建,而同步脚本可能阻塞;不能把所有外部资源统一归类为“阻塞解析”。动态插入的脚本及模块脚本也有不同的调度语义,应按具体类型判断。
工程验证: 可在脚本前后分别调用 document.getElementById,观察后置节点在同步脚本执行时是否为 null;再将脚本改为 defer 对比结果。开发者工具的网络瀑布图能确认脚本等待时间,性能面板中的解析事件、脚本任务和 DOMContentLoaded 时间则可用于确认阻塞发生在哪一段。
面试官追问
追问 1接口监控显示首页 HTML 在几十毫秒内返回,前端负责人因此断言首屏迟迟不出现一定与文档解析无关,你会如何反驳?
这个结论不成立,网络层交付的只是字节流,浏览器仍要完成字符转换、分词和树构建才能得到 DOM。同步脚本还可能暂停解析,因此响应快速不等于结构及时可用;最终首屏时间也会受后续渲染环节影响。
追问 2一个服务端输出的长页面包含多个同步外部脚本,你会怎样调整脚本加载,既减少树构建阻塞又保证依赖完整文档的代码能运行?
可优先评估将经典脚本标记为 defer,使其下载时不阻塞文档解析,并在解析完成后、DOMContentLoaded 前执行。依赖关系与执行顺序需要按脚本类型验证;若代码依赖解析过程中的即时写入,直接改造可能改变行为。
追问 3同一段脚本从文档末尾移到首个内容节点之前后,调用 document.getElementById 得到 null,代码本身没有变化,原因是什么?
同步脚本执行时只能可靠访问已经完成解析的前置节点,位于脚本后的元素尚未加入 DOM。移动位置改变了脚本执行时可见的树,而不是选择器语义;可改用合适的加载时序,但要留意由此带来的执行顺序变化。
追问 4线上性能面板显示 DOMContentLoaded 明显推迟,同时网络瀑布图里一个经典脚本等待很久,你会怎样确认它是否阻塞了 DOM 构建?
应对齐网络瀑布中的脚本等待区间、性能面板中的解析事件和脚本任务,检查解析是否在脚本取得与执行期间暂停。还可在脚本前后探测后置节点是否存在,并改为 defer 做对照;图片等外部资源通常不阻塞 DOM,不能把所有请求一起归因。
追问 5安全评审把跨站图片、跨站脚本和跨站接口统一标成“都会改变 DOM 构建算法并阻塞解析”,你会怎样纠正?
资源来源不同不会改变 DOM 的基本树构建算法,但会引入网络延迟、同源策略、CORS 或内容安全策略等约束。同步经典脚本可能阻塞解析,图片通常不会;动态脚本和模块脚本还具有不同调度语义,必须按资源类型判断。
# 什么是 DOM
30 秒速记
DOM是浏览器把HTML字节内容转换后得到的内部结构化表示,也是生成页面的基础数据。- 对脚本而言,
DOM提供访问和修改文档的接口,使JavaScript能改变结构、样式与内容。 - 对渲染引擎而言,原始网络字节流不能直接使用,必须先转成可理解、可处理的
DOM结构。 - 在安全层面,解析阶段会过滤或拒绝部分不安全内容,因此
DOM也承担一道输入防护职责。 DOM同时连接文档、脚本与渲染流程,频繁或错误的修改会直接改变页面最终状态。
DOM 是浏览器对 HTML 文档的内部结构化表示,也是页面、脚本和渲染流程之间的连接层。 网络传来的字节流不能直接用于渲染,必须先转换成浏览器能够理解和处理的树形结构。对 JavaScript 来说,它提供了一套访问和修改文档的接口,因此脚本可以改变页面结构、样式和内容。解析阶段还会拒绝部分不安全内容,所以 DOM 在生成页面之外,也承担了一层输入防护作用。
从网络传给渲染引擎的 HTML 文件字节流是无法直接被渲染引擎理解的,所以要将其转化为渲染引擎能够理解的内部结构,这个结构就是 DOM。DOM 提供了对 HTML 文档结构化的表述。在渲染引擎中,DOM 有三个层面的作用
- 从页面的视角来看,DOM 是生成页面的基础数据结构。
- 从 JavaScript 脚本视角来看,DOM 提供给 JavaScript 脚本操作的接口,通过这套接口,JavaScript 可以对 DOM 结构进行访问,从而改变文档的结构、样式和内容。
- 从安全视角来看,DOM 是一道安全防护线,一些不安全的内容在 DOM 解析阶段就被拒之门外了。
简言之,DOM 是表述 HTML 的内部数据结构,它会将 Web 页面和 JavaScript 脚本连接起来,并过滤一些不安全的内容。
面试官追问
追问 1后端同学把接口返回的 200 KB HTML 字符串直接称为“已经生成的 DOM”,并据此判断浏览器无需再处理,你会怎么纠正?
这段内容仍是网络传来的字节或文本,并不是渲染引擎可直接使用的 DOM。浏览器需要解析它并建立内部节点结构,页面生成和脚本操作才有基础;解析成本与最终渲染成本也不能混为一谈。
追问 2一个富文本编辑页通过脚本修改节点内容和样式,产品要求同时把服务器上的原始模板永久改掉,你会如何界定前端改动的作用范围?
脚本通过 DOM 接口修改的是当前文档中的结构、样式或内容,不会自动反向写回服务器模板。若要持久化,必须另行采集变更并提交给后端;保存格式、权限和冲突处理不属于 DOM 自身提供的能力。
追问 3同一份页面既要供渲染引擎生成画面,又要让埋点脚本读取节点层级,是否需要维护两套独立结构?
通常不需要,DOM 本身就是页面生成的基础数据结构,同时向 JavaScript 提供访问和修改文档的接口。两类消费者共享这份结构,但脚本修改会改变当前文档状态;由此引起的后续渲染影响需要结合具体改动判断。
追问 4安全事故中服务端返回的页面片段含有危险内容,团队认为只要网络请求成功,所有内容就必然进入页面内部结构,你会先检查什么?
应检查内容在解析并形成 DOM 时是否被拒绝,以及实际生成的节点结构,而不能只看响应正文。来源指出 DOM 解析承担一定安全过滤作用;具体拦截范围取决于浏览器实现,仍需配合其他安全策略,不能把它当作完整防线。
追问 5代码评审中有人建议绕过 DOM,让渲染引擎直接读取服务器返回的 HTML 字节流,以减少一层转换,你会接受吗?
不会,网络字节流无法直接作为渲染引擎理解的页面结构,必须转化为内部的 DOM 表述。DOM 还连接页面与 JavaScript,并承担部分内容过滤职责;绕过它会同时失去结构化处理、脚本接口与这道安全边界。
# DOM 树如何生成
30 秒速记
HTMLParser不会等待整个文档下载完毕,而是持续消费网络进程送来的字节流,边接收边构建DOM。- 浏览器根据响应头的
content-type判断资源类型;对text/html,网络进程与渲染进程通过数据管道持续传递内容。 - 解析首先把字节流切分为开始标签、结束标签和文本等
Token,随后同步完成节点创建与入树。 - 解析器用栈维护尚未闭合的元素:开始标签创建节点并入栈,文本生成文本节点但不入栈,匹配的结束标签使对应开始标签出栈。
- 解析开始时已有
document根节点;新节点依据当前栈中的层级关系确定父节点,直到全部字节流处理完成。 - 真实页面还会混入脚本、样式和媒体资源,因此基础栈模型解释结构生成,但不能覆盖所有阻塞与资源调度细节。
DOM 树由 HTMLParser 边接收网络字节流边解析生成,不需要等整个文档下载完成。 浏览器先根据 content-type 识别 text/html,再把数据持续交给渲染进程,将内容拆成开始标签、结束标签和文本等 Token。解析器通过栈维护节点层级:开始标签创建节点并入栈,文本生成文本节点,结束标签让匹配元素出栈。这个模型能说明基本结构生成,但真实页面还包含脚本、样式和媒体资源,是否阻塞要按资源类型判断。
