排版引擎(也常被称作浏览器内核)负责把网络接收到的资源转换成可交互的页面。它并不是一块单一的程序,而是由若干职责明确的模块按层组织,模块之间通过约定好的数据结构传递结果。理解每一层的输入与输出,是阅读引擎实现或分析页面行为的起点。
多数实现把内核划分为资源获取、解析、样式、布局、绘制与合成等几个部分,并由一套任务调度与内存管理设施在背后支撑。层的划分思路在不同实现之间大致相似,差异主要集中在模块边界的划定与内部数据结构的选择上。
分层的职责划分
按数据流动方向,可以把内核划分为如下几层:
| 层 | 主要输入 | 主要输出 |
|---|---|---|
| 网络与资源加载 | URL、请求参数 | 字节流、解码后的资源 |
| 解析 | 字节流 | DOM 树、CSSOM、脚本 |
| 样式 | DOM 树、CSSOM | 带计算样式的渲染树 |
| 布局 | 渲染树 | 带几何信息的盒树 |
| 绘制 | 盒树 | 绘制指令列表 |
| 合成 | 绘制指令、图层 | 纹理与合成帧 |
每一层的输出会被缓存并在后续复用,只有当输入发生变化时,对应的层才需要重新计算。这种「按层失效」的组织方式,使得一次局部变更不必推动整条链路重跑。
网络与资源加载
资源加载层接受解析层提出的请求,按优先级排队并复用连接。它处理的资源类型包括 HTML 文档、样式表、脚本、字体与图片。加载层需要决定请求的先后顺序:文档与阻塞渲染的样式表通常被赋予较高优先级,视口内的图片优先级高于视口外的图片。
GET /index.html HTTP/2
accept: text/html
accept-encoding: gzip, br
响应到达后,加载层负责解压、确定字符编码,并把字节流交给解析层。对于可流式处理的资源,字节流边到达边交付,不必等待完整响应。
解析与样式
解析层包含 HTML 解析器、CSS 解析器以及脚本引擎的调用入口。HTML 解析器以流式方式消费字符,维护一棵 DOM 树与一个待处理记号栈。遇到 <script> 时,解析器需要根据脚本的加载方式决定是否暂停自身。
样式层把 CSSOM 与 DOM 树结合,为每个元素确定生效声明。选择器匹配需要沿祖先链比对,因此引擎通常会维护若干索引(例如按 id、类名、标签名建立的映射)来缩小候选范围,避免每次都遍历整棵树。
布局、绘制与合成
布局层根据计算样式与内容生成盒模型树,输出每个盒的位置与尺寸。布局的求解依赖父级给出的约束,因此一次自上而下的遍历配合自下而上的尺寸回传,即可完成多数情况下的计算。
绘制层不直接写像素,而是产生有序的绘制指令;合成层把指令按图层栅格化为纹理,再交给图形接口合成输出。把页面按层叠上下文划分为独立图层,可以让局部变更只触发该图层的重新栅格化。
多进程模型与进程间通信
现代内核普遍采用多进程组织:浏览器主进程负责窗口、导航与权限,渲染进程负责某个站点(或某组站点)的文档渲染,此外通常还有网络进程与图形进程各自承担专门的工作。
进程之间不共享地址空间,因此需要通过进程间通信传递消息。渲染进程一般不会直接发起网络请求,而是把请求交给网络进程;与合成相关的任务则交给图形进程。消息的序列化与线程切换都有成本,因此引擎倾向于把高频操作合批处理,减少往返次数。
站点隔离 的做法是让不同站点运行在不同的渲染进程中,使一个页面的渲染进程无法直接读取另一个站点的内存。代价是进程数量增加,引擎需要在资源占用与隔离强度之间做出权衡。
公共基础设施
跨模块复用的设施大致有三类:
- 任务调度:把来自解析、布局、脚本、定时器与用户输入的工作拆成任务,按优先级与时限排队执行,与 事件循环 的模型相互对应。
- 内存管理:为 DOM 节点、样式对象与布局对象提供统一的生命周期管理,多数实现以垃圾回收配合分代策略或引用计数来处理。
- 事件分发:把输入设备的事件按命中测试结果派发到目标元素,并在捕获与冒泡阶段依次调用已注册的监听器。
小结
内核的架构可以概括为「分层加多进程」:分层确定了单次渲染的数据流向,多进程确定了稳定性与安全性的边界。在分析具体行为时,先定位它发生在哪一层,再确认它跨越了哪些进程边界,问题的范围通常就能收敛到一个可验证的假设。