深入浅出浏览器渲染原理
Category(分类): Browser Status: 已更新
浏览器内核通常可以从两个角度理解:负责解析和绘制页面的渲染引擎,以及负责执行 JavaScript 的引擎。不同浏览器的实现不同:Firefox 使用 Gecko/SpiderMonkey,Safari 使用 WebKit/JavaScriptCore,现代 Chrome/Edge 使用 Blink/V8。本文保留早期 WebKit 资料的学习路线,但会把旧版 WebKit 内部类名和现代浏览器的通用模型区分开。
一、浏览器工作的整体流程
浏览器加载页面的大体路径可以抽象为:
字节
-> 字符解码
-> HTML tokenizer / tree builder
-> DOM
-> CSS 解析和计算样式
-> 布局(Layout)
-> 绘制记录(Paint / Display List)
-> 栅格化(Raster)
-> 合成(Composite)
-> 屏幕像素

这不是每一帧都会完整执行的固定清单。浏览器会并行下载资源,并根据变化范围跳过不需要的阶段;例如只改变合成属性的动画可能绕过布局和绘制,图片尺寸变化则可能触发布局和后续绘制。

二、解析什么内容
1. HTML、SVG 和 XHTML
HTML 解析会产生 DOM。SVG 作为 HTML 文档中的内容也会形成 DOM 节点;独立 SVG 文档有自己的文档类型和解析规则。XHTML 以 XML MIME 类型提供时遵循 XML 解析规则,错误处理方式与 HTML 不同。
DOM 是浏览器内部表示文档结构的数据,也通过 DOM API 暴露给 JavaScript。DOM 并不等于最终绘制结构:display: none 节点通常不参与布局,伪元素、匿名盒和部分替换元素也可能只出现在布局或绘制结构中。
2. CSS
CSS 会被解析为规则,浏览器结合来源、层叠、继承、媒体查询、容器查询和自定义属性计算每个元素的最终样式。早期 WebKit 文档中的 CSS Rule Tree 是实现细节,现代引擎可能使用完全不同的样式数据结构;开发者应关注 CSSOM、计算样式和层叠结果,而不是依赖某个旧类名。
3. JavaScript
JavaScript 通过 DOM、CSSOM、Canvas、Web APIs 等接口与页面交互。JavaScript 引擎会把源代码解析为内部表示,并可能解释执行、编译为字节码或使用 JIT 优化;这些策略属于引擎实现,不应简单概括成“JavaScript 只是解释型语言”。
三、构建 DOM
浏览器会把网络或磁盘中的 HTML 字节按照 BOM、HTTP 头、meta charset 和规范的编码嗅探规则解码为字符,然后经过 tokenizer 和 tree builder 构建 DOM。

1. 标记化
例如:
<html>
<head>
<title>Web page parsing</title>
</head>
<body>
<h1>Web page parsing</h1>
<p>This is an example Web page.</p>
</body>
</html>
解析器会识别 DOCTYPE、开始标签、结束标签、属性、文本、注释和文件结束等 token。token 不只是字符串,还包含“开始标签/结束标签/文本”等类型信息。

2. 建树和错误恢复
HTML tokenizer 产生 token 后,tree builder 会根据当前插入模式、开放元素栈和活动格式化元素把它们插入文档树。结束标签通常用于改变解析状态或弹出开放元素,不会像开始标签那样再创建一个元素节点。
字符 -> token -> DOM 节点 -> DOM 树
HTML 标准定义了对缺少闭合标签、错误嵌套、表格、form、脚本数据和实体等情况的错误恢复规则。浏览器通常不会因为普通 HTML 错误直接抛出异常,但这不代表错误标记可以安全依赖;应尽量输出规范、可预测的 HTML。

3. 解析不是“等全部完成后再建树”
浏览器通常边接收、边解码、边标记化、边建树。页面可能在 HTML 尚未下载完时就开始请求 CSS、脚本、图片和字体,也可能在资源未完成时先显示部分内容。
预加载扫描器会提前发现资源并发起请求,但它不会改变 HTML 解析器的语义。资源优先级、缓存、连接复用和 fetchpriority 会影响实际请求顺序。
四、构建 CSSOM 和计算样式
DOM 只描述内容和关系,浏览器还需要知道元素如何显示。CSSOM 是脚本可访问的 CSS 对象模型及样式表规则的统称;内部的计算样式和布局数据则由浏览器自行组织。
CSS 的处理大致包括:
- 解析 CSS 文本和规则;
- 处理
@import、媒体查询、层叠层和自定义属性; - 为元素匹配选择器;
- 处理继承、初始值和层叠优先级;
- 生成计算样式。
浏览器内置的用户代理样式表会让没有作者 CSS 的 h1、button 等元素仍然具有默认外观。选择器匹配和样式计算需要成本,但不应机械地把“选择器越长越慢”当成唯一优化方向;应先用 Performance 和实际 DOM 规模定位瓶颈,优先减少无意义节点、频繁样式失效和强制同步布局。
五、构建渲染/布局结构
教学资料常把 DOM 与 CSSOM 组合成 Render Tree。这个模型可以帮助理解哪些内容最终可见,但现代浏览器的真实结构更加复杂:Chromium RenderingNG 会使用计算样式、布局 fragment tree、属性树、绘制块、显示列表和合成层列表等数据。

通常:
display: none的节点不参加布局,也不会产生普通绘制内容;visibility: hidden的节点通常仍参加布局,只是不绘制可见内容;opacity: 0的元素通常仍参加布局并可能参与绘制/合成;- 伪元素可以参与布局和绘制,但不是 DOM 子节点,不能用普通 DOM API 查询为元素节点;
- iframe、shadow tree、匿名盒、列表标记和替换元素会让 DOM 与布局结构不一一对应。
“渲染树只包含显示节点”是教学简化,不代表所有不可见或辅助结构都会在浏览器内部被完全删除。
六、布局(Layout)与绘制(Paint)
1. 布局
布局是根据计算样式、包含块、视口、字体、内容和布局算法计算尺寸与位置的过程。Flex、Grid、表格、浮动、绝对定位、文本换行、滚动容器和书写模式都会影响布局。
第一次计算尺寸和位置通常称为 initial layout;后续几何变化需要重新计算时,传统资料常称为 reflow,现代 DevTools 更常显示 Layout。

布局的输出不一定是一个简单的“每个元素一个矩形”。现代引擎还要处理多列、分页、行盒、fragmentation、滚动和子框架等情况。
2. 绘制
布局完成后,浏览器按照层叠顺序记录背景、边框、文本、图片、阴影、滤镜和其他视觉效果的绘制操作。Paint 主要是生成绘制记录;Raster 则是执行这些记录,把内容转换为图块或纹理,这两个阶段不要混为一谈。

如果只改变颜色、背景或阴影,通常可以跳过布局但仍需要绘制。改变宽度、字体、内容或定位可能影响布局,并进一步导致绘制和合成更新。实际失效范围由浏览器的增量布局、绘制缓存和内容结构决定。
3. 合成
浏览器可能把视频、canvas、滚动内容或正在执行合成属性动画的元素放入独立合成资源。合成线程根据属性树、裁剪、变换、透明度和滚动偏移组成一帧。
独立合成资源不等于“元素所有工作都在 GPU 上完成”。主线程仍可能执行 JavaScript、样式、布局和绘制;合成层过多或纹理过大还会增加内存和上传成本。
七、问题一:遇到 JavaScript 会发生什么
1. 经典脚本
遇到没有 async 或 defer 的外部经典脚本时,HTML 解析器通常会暂停,等待脚本下载、解析和执行:
<script src="classic.js"></script>
原因不仅是脚本可以修改 DOM,还包括 document.write() 等历史 API 可能改变解析输入。内联经典脚本也会同步执行,但没有网络下载步骤。
2. defer、async 和模块脚本
<script src="app-a.js" defer></script>
<script src="app-b.js" defer></script>
<script type="module" src="main.js"></script>
<script src="metrics.js" async></script>
defer脚本在后台下载,HTML 解析完成后执行,多个 defer 经典脚本保持顺序;- 模块脚本默认延迟执行,依赖模块加载完成后按模块图执行;
async脚本下载完成就执行,不能依赖多个 async 脚本的顺序;async可能在DOMContentLoaded前或后执行,但通常会阻塞load事件;DOMContentLoaded会等待 defer 和模块脚本,不会等待 async 脚本、图片和所有懒加载资源;defer对内联经典脚本没有普通外部脚本意义,动态插入脚本的行为也需要单独判断。
“把所有 script 放在 body 底部”是历史上简单有效的经验,但现代项目更应根据脚本类型使用 defer、模块、代码分割和按需加载。非关键分析脚本可以使用 async,但要保证其独立性。
3. CSS 是否阻塞 DOM
CSS 下载不会让 HTML tokenizer 必然停止,浏览器可以继续解析和下载资源。但样式表通常会阻塞首次绘制;当一个经典脚本可能查询或修改样式时,浏览器可能等待此前已发现的阻塞样式表完成,以提供一致的脚本观察结果。因此更准确的说法是:
CSS 主要阻塞渲染,并可能延迟依赖样式信息的脚本执行;脚本又可能暂停 HTML 解析。
八、问题二:回流和重绘
1. 回流/布局
可能影响布局的操作包括:
- 添加、删除或移动参与布局的节点;
- 改变宽高、内外边距、边框、定位、字体、文本和内容;
- 改变视口尺寸、容器尺寸或布局方向;
- 图片、字体等资源加载后改变内容尺寸;
- 读取布局信息时,浏览器为了返回最新值而强制刷新未完成的样式或布局。

初次布局不是“回流”;回流通常指初次布局之后再次计算几何信息。布局可能是局部的,也可能传播到祖先、兄弟或整个文档,取决于布局模式和包含关系。
2. 重绘
改变颜色、背景、边框、阴影、文本外观等而不改变几何关系时,通常会触发 Paint/Raster,而不必重新布局。绘制面积大、滤镜复杂、阴影范围大或高 DPR 纹理较多时,重绘也可能很昂贵。
“回流必定重绘、重绘不一定回流”作为简化模型有参考价值,但浏览器可能使用缓存、合成和增量失效,不能根据一条 CSS 属性就断言一定会整页重绘。
3. 避免布局抖动
const items = [...document.querySelectorAll('.item')]
// 先集中读取
const widths = items.map(item => item.getBoundingClientRect().width)
// 再集中写入
items.forEach((item, index) => {
item.style.setProperty('--measured-width', `${widths[index]}px`)
})
不要在高频循环中交错读写布局:
for (const item of items) {
item.style.width = `${item.offsetWidth + 1}px`
}
可以批量修改 class、使用脱离文档的节点、CSS containment 或 content-visibility,但要验证焦点、可访问性、滚动尺寸和首屏体验。transform 常适合做位移动画,但不是所有 transform 都只走合成。
九、问题三:async 和 defer 的区别

| 写法 | 下载 | 执行时机 | 顺序 | 对 DOMContentLoaded 的影响 |
|---|---|---|---|---|
| 普通经典脚本 | 解析遇到后下载 | 下载完成立即执行 | 按遇到顺序 | 执行完成后才能继续 |
async | 与解析并行 | 下载完成立即执行 | 不保证 | 不等待它触发,但它通常影响 load |
defer | 与解析并行 | 解析完成后执行 | 经典 defer 之间按文档顺序 | 会等待 |
type="module" | 按模块图加载 | 默认延迟执行 | 按依赖图 | 会等待模块执行 |
async 适合独立的统计和广告脚本;defer 适合依赖 DOM、需要顺序的经典脚本;模块脚本适合现代依赖图。最终仍要结合脚本大小、优先级、缓存和真实页面指标验证。
十、问题四:为什么有人觉得操作 DOM 很慢
把原因简单归结为“DOM 属于渲染引擎、JS 属于 JS 引擎,所以每次操作都要跨线程通信”是不准确的。页面 JavaScript 和 DOM 操作通常都在渲染进程的主线程执行,性能成本主要来自:
- 操作节点数量和数据规模过大;
- 样式失效和选择器匹配;
- 触发布局、绘制、栅格化或合成;
- 读写交错导致强制同步布局;
- JavaScript 长任务阻塞同一个主线程;
- 大量 IPC、资源解码或跨进程 iframe 协作。
因此,优化重点是减少无效更新、批量读写、缩小影响范围和把合适的计算迁移到 Worker,而不是完全避免 DOM。
十一、问题五:白屏、FOUC 与页面性能
FOUC(Flash of Unstyled Content)是内容先以无样式或不完整样式显示,随后样式加载后改变外观。它不是某个浏览器固定的专属现象,可能由 CSS 延迟、样式切换、字体、SSR 水合或脚本逻辑造成。
“浏览器一定先构建完整 DOM/CSSOM 才绘制”也不准确。浏览器可以增量解析和绘制;首屏显示取决于关键 CSS、HTML、脚本、字体、图片、网络和渲染调度。白屏还可能来自:
- CSS 或脚本阻塞首屏;
- JavaScript 长任务或运行时错误;
- SSR 返回空壳并等待水合;
- 字体、图片和第三方资源阻塞关键内容;
- 页面实际发生了导航错误或安全拦截。
建议把关键 CSS 放在合理位置,延迟非关键脚本,使用服务端错误监控和真实用户监控,分别观察 FCP、LCP、INP、CLS 和错误率,而不要只凭“Chrome/Firefox 会不会白屏”的旧结论。
十二、总结

- 浏览器通常边接收边解析 HTML,并通过预加载扫描器发现资源;
- DOM、CSSOM、计算样式、布局和绘制是相互关联但不完全等同的结构;
- 初次布局与后续回流要区分,布局之后还可能发生 Paint、Raster 和 Composite;
- 经典脚本、
async、defer和模块脚本的阻塞与顺序不同; - JavaScript 操作 DOM 慢不只是因为“跨线程通信”,更常见的原因是长任务、布局抖动和过大的更新范围;
- 性能优化应使用 DevTools Performance、Network 和真实用户指标验证,而不是把历史经验当成固定规则。