技术知识文章集合TECHNICAL ARCHIVE · 457 DOCUMENTS

显示模式

登录
ARCHIVE DOCUMENTBR

深入浅出浏览器渲染原理

所属馆藏
Browser
文件格式
Markdown
原始路径
Browser/35-深入浅出浏览器渲染原理
本文目录13 个章节
  1. 一、浏览器工作的整体流程
  2. 二、解析什么内容
  3. 三、构建 DOM
  4. 四、构建 CSSOM 和计算样式
  5. 五、构建渲染/布局结构
  6. 六、布局(Layout)与绘制(Paint)
  7. 七、问题一:遇到 JavaScript 会发生什么
  8. 八、问题二:回流和重绘
  9. 九、问题三:async 和 defer 的区别
  10. 十、问题四:为什么有人觉得操作 DOM 很慢
  11. 十一、问题五:白屏、FOUC 与页面性能
  12. 十二、总结
  13. 参考资料

深入浅出浏览器渲染原理

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。

HTML 字节到 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 不只是字符串,还包含“开始标签/结束标签/文本”等类型信息。

HTML token 与节点关系

2. 建树和错误恢复

HTML tokenizer 产生 token 后,tree builder 会根据当前插入模式、开放元素栈和活动格式化元素把它们插入文档树。结束标签通常用于改变解析状态或弹出开放元素,不会像开始标签那样再创建一个元素节点。

字符 -> token -> DOM 节点 -> DOM 树

HTML 标准定义了对缺少闭合标签、错误嵌套、表格、form、脚本数据和实体等情况的错误恢复规则。浏览器通常不会因为普通 HTML 错误直接抛出异常,但这不代表错误标记可以安全依赖;应尽量输出规范、可预测的 HTML。

DOM 树示意

3. 解析不是“等全部完成后再建树”

浏览器通常边接收、边解码、边标记化、边建树。页面可能在 HTML 尚未下载完时就开始请求 CSS、脚本、图片和字体,也可能在资源未完成时先显示部分内容。

预加载扫描器会提前发现资源并发起请求,但它不会改变 HTML 解析器的语义。资源优先级、缓存、连接复用和 fetchpriority 会影响实际请求顺序。

四、构建 CSSOM 和计算样式

DOM 只描述内容和关系,浏览器还需要知道元素如何显示。CSSOM 是脚本可访问的 CSS 对象模型及样式表规则的统称;内部的计算样式和布局数据则由浏览器自行组织。

CSS 的处理大致包括:

  1. 解析 CSS 文本和规则;
  2. 处理 @import、媒体查询、层叠层和自定义属性;
  3. 为元素匹配选择器;
  4. 处理继承、初始值和层叠优先级;
  5. 生成计算样式。

浏览器内置的用户代理样式表会让没有作者 CSS 的 h1button 等元素仍然具有默认外观。选择器匹配和样式计算需要成本,但不应机械地把“选择器越长越慢”当成唯一优化方向;应先用 Performance 和实际 DOM 规模定位瓶颈,优先减少无意义节点、频繁样式失效和强制同步布局。

五、构建渲染/布局结构

教学资料常把 DOM 与 CSSOM 组合成 Render Tree。这个模型可以帮助理解哪些内容最终可见,但现代浏览器的真实结构更加复杂:Chromium RenderingNG 会使用计算样式、布局 fragment tree、属性树、绘制块、显示列表和合成层列表等数据。

DOM、CSSOM 和渲染结构

通常:

  • 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. 经典脚本

遇到没有 asyncdefer 的外部经典脚本时,HTML 解析器通常会暂停,等待脚本下载、解析和执行:

<script src="classic.js"></script>

原因不仅是脚本可以修改 DOM,还包括 document.write() 等历史 API 可能改变解析输入。内联经典脚本也会同步执行,但没有网络下载步骤。

2. deferasync 和模块脚本

<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 的区别

async、defer 与 HTML 解析时序

写法下载执行时机顺序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;
  • 经典脚本、asyncdefer 和模块脚本的阻塞与顺序不同;
  • JavaScript 操作 DOM 慢不只是因为“跨线程通信”,更常见的原因是长任务、布局抖动和过大的更新范围;
  • 性能优化应使用 DevTools Performance、Network 和真实用户指标验证,而不是把历史经验当成固定规则。

参考资料

457 DOCUMENTS · 10 COLLECTIONS
ARCHIVE SEARCH457 篇文章

SEARCH GUIDE

输入关键词开始搜索

支持搜索文章标题、所属分类和原始文档路径。

按分类浏览

10 COLLECTIONS