Appearance
浏览器内核渲染细节:从失效到 GPU 合成
返回专题索引:客户端渲染:从状态到画面。先读:浏览器运行时与帧调度。
本页回答一个更具体的问题:一次 DOM/CSS 变化之后,浏览器怎样避免“把整页从头画一遍”,又在哪些情况下不得不重新算、重新画或重新上传纹理?答案不是某一个固定数据结构。下文区分 Web 的通用渲染概念 与 Chromium/Blink 的一个重要实现实例;Firefox 和 WebKit 也有对应职责,但树、缓存、线程和启发式不同。
1. 先看对象如何变成像素
通用因果链如下:
text
HTML + CSS + viewport + 字体/图片等资源
-> 文档结构与计算后的样式
-> 几何布局结果(boxes/fragments)
-> 绘制命令与可重用的绘制片段
-> 图层/表面、图块与位图纹理
-> GPU/显示合成
-> 屏幕像素 + 命中测试区域不要把 DOM 当作屏幕对象。DOM 是作者描述和脚本操作的树;它会经过 CSS 选择、生成盒子、分页/分栏/行内断行、裁剪与叠放上下文等转换。display: none、伪元素、匿名盒、文本片段都说明“一个 DOM 节点对应一个矩形层”并不成立。
| 阶段 | 通用职责 | 修改后最常见的代价 |
|---|---|---|
| 样式 | 决定元素应采用哪些规则和继承值 | style recalculation |
| 布局 | 求尺寸、位置、断行、滚动范围 | layout / reflow |
| 绘制 | 把背景、文本、边框、图片等变成绘制指令 | paint / record |
| 栅格 | 将指令在给定缩放下转为像素图块 | raster、纹理上传 |
| 合成 | 按裁剪、变换、透明度和顺序组合结果 | compositor / GPU work |
2. Blink/Chromium 的实例:中间表示并非一棵树
以下术语来自 Blink/Chromium,适合读 trace 或源码资料时建立映射;它们不是 Web 平台 API。
text
DOM
-> computed style
-> LayoutObject + fragments(布局对象/片段)
-> display items / paint chunks(绘制指令分组)
-> property trees(transform / clip / effect / scroll)
-> compositor layers / composited layer tree
-> tiles -> raster -> compositor quadsLayoutObject 与 fragments
Blink 用 LayoutObject 表示参与布局的对象;现代布局还会产出 fragments,用于表达一段内容在不同格式化上下文、分栏、分页或行内断行中的实际几何。概念上这解决了“一个元素只有一个框”的错误模型:一个文本节点可以有多行片段,一个元素可在多个 fragmentainer 中出现。
Paint property trees 与 paint chunks
Chromium 的绘制管线会将变换(transform)、裁剪(clip)、效果(effect,例如 opacity)和滚动(scroll)等属性组织为 property trees,并将有相近属性状态的 display items 分为 paint chunks。这样合成器能理解“同一批绘制内容处于什么变换、裁剪和透明度环境”,并尽量复用未受影响的结果。
这是实现细节,不应据此假定每个 CSS 属性都会生成一棵可见的树,或认为 paint chunk 等于 DOM 节点/图层。它们是为了缩小失效范围和传递合成信息的中间表示。
3. 失效:先问什么旧结果不可信
所谓 invalidation(失效)不是错误,而是浏览器标记缓存结果不能继续使用的过程。优化的核心不是“永不失效”,而是让变化只传播到逻辑上必须重算的范围。
text
DOM/CSS/资源变化
-> 哪些 computed style 可能变? style invalidation
-> 哪些几何依赖可能变? layout invalidation
-> 哪些像素内容不再正确? paint invalidation
-> 哪些图块/纹理需要更新? raster invalidation
-> 旧层如何重新排序/裁剪/混合? compositing update样式失效
样式失效发生在选择器匹配、继承、自定义属性、伪类状态或样式表变化可能改变计算样式时。例如给祖先加 class,可能影响大量后代;修改仅由局部规则命中的 class,范围可能较小。浏览器会做选择器和继承优化,但作者不能把“一个 class 修改”当作固定 O(1)。
contain: style、合理的组件边界和避免极广泛的后代选择器有时能限制影响,但它们有语义和兼容性约束。先量测 style recalculation 的范围和时间,再改变 CSS 架构。
布局失效
布局依赖可用空间和相邻/祖先几何。改变 width、font-size、内容、display、网格轨道或文档流位置,常会让一个格式化上下文内其他对象重算。不是每个“几何属性”都必然触发全页布局,布局包含、增量算法和独立格式化上下文可缩小范围,但范围必须由实际 trace 验证。
强制同步布局是另一类问题:脚本先写导致布局变脏,再立刻读取几何(如 offsetTop)时,浏览器为了给出正确值,可能在 task 中提前执行布局。反复读写交错会造成 layout thrashing:
text
坏:write -> read geometry -> write -> read geometry
好:read all geometry -> compute -> write all mutations绘制失效
即使几何不变,颜色、阴影、背景、文字内容、border、canvas 内容变化仍可能让像素失效,需要重新记录或重放绘制指令。受影响面积、复杂裁剪、文字和滤镜决定 cost;“只改颜色”并不代表免费。
4. 分层不等于“给元素加 GPU”
为了让部分内容可以独立滚动、变换、透明混合或避免重绘,浏览器会把适合的内容提升或组织进合成层。Chromium 的 layerization 是启发式和版本相关的,开发者不能要求某元素永久拥有独立 GPU layer。
text
paint chunks + property state
-> compositor decides layer boundaries
-> layer is split into viewport-sized tiles
-> tiles rasterized into textures
-> compositor orders/scales/clips/blends textures| 误解 | 实际边界 |
|---|---|
transform: translateZ(0) 总会加速 | 可能改变分层,但也增加纹理、内存和合成;不是稳定 API |
| 每个 CSS layer 都是 compositor layer | CSS cascade layer 与图形合成层无关 |
| 有独立层就不会 paint | 内容变化仍需重新绘制/栅格;独立层只改变工作隔离方式 |
| GPU 一定比 CPU 快 | 大纹理、过度绘制、滤镜、上传和带宽可能使 GPU 成为瓶颈 |
will-change 是给浏览器的短期提示,适用于即将变化且已测量证实受益的属性。长期给大量元素设置它,可能提前占用内存、增加分层和合成负担;完成动画应移除提示。
5. 图块、栅格与 GPU 合成
屏幕通常只需要视口附近的内容,合成器会把大的可绘制区域切成 tiles。图块可并行栅格,按优先级准备当前视口、预测滚动方向附近和低优先级离屏内容。栅格结果成为 GPU 可使用的纹理或等价资源,然后合成器生成按 z-order、clip、transform、opacity 排列的 quads 提交显示。
text
large scrollable paint area
+-------------------------------+
| tile A | tile B | tile C | <- raster independently
| tile D | tile E | tile F |
+-------------------------------+
viewport
+----------------+
| B / C / E / F | <- prioritized now
+----------------+滚动时若新进入视口的 tiles 尚未栅格完成,可能看到空白/低清图块或短暂卡顿;如果已经存在所需纹理且滚动可由合成器处理,主线程即使忙,画面仍可能移动。高分辨率、大图片、复杂 SVG、滤镜和大量半透明重叠都会扩大 raster/GPU 成本。
6. 滚动与命中测试不是渲染后的附属品
渲染提交会携带滚动节点、裁剪和可交互区域等信息。合成器可利用这些信息执行部分滚动,输入系统则通过 hit test 把坐标映射到目标区域。若页面存在可取消的触摸/滚轮监听器、复杂 iframe 边界,或需要主线程脚本决定默认行为,输入可能回到主线程等待。
几何和交互也会失配:视觉上用 transform 移动元素,命中测试通常会跟随相应的变换;但用覆盖层、pointer-events、裁剪、滚动容器或异步状态更新组合时,用户可见目标、实际 hit target 与可访问性焦点可能不同。诊断不能只看截图,应实际点击、Tab 导航并检查 Accessibility tree。
7. 关键渲染路径:资源何时允许首帧成立
渲染不是只从 DOM mutation 开始。导航后的首帧要经过网络、解析和资源优先级:
text
navigation
-> HTML bytes -> parser -> DOM
-> CSS discovery/fetch -> CSSOM
-> DOM + CSSOM -> style/layout -> first paint
-> image/font/script discovery and fetch
-> later layout/paint/composite as resources resolve外部样式表通常会阻塞依赖其样式的首次渲染,因为没有 CSSOM 无法可靠计算样式。同步脚本可阻塞 HTML 解析;defer 在解析后执行,async 按下载完成时机执行,二者改变依赖与执行顺序,不能只按“更快”选择。字体下载可能导致文本延迟显示或 fallback 后再重排;图片的尺寸信息、width/height 或 aspect-ratio 决定资源到达后是否造成布局位移。
资源优先级是浏览器启发式与提示共同决定的结果。preload、fetchpriority 等只能用于已证明处于关键路径的资源;预加载过多会抢占真正关键资源。用 Network waterfall 和 LCP/布局位移证据校验,而不是只堆提示。
8. 证据:在 DevTools 和 trace 中应看到什么
Chrome DevTools 对 Chromium 的内部信息最直接;其他浏览器应使用各自 profiler 获取同类证据,而不能凭 DevTools 的某个标签推断所有内核。
| 问题假设 | 观察工具 | 支持或推翻它的证据 |
|---|---|---|
| JS 把输入堵住 | Performance 主线程、Long tasks、Event Log | 输入前等待长,call tree 指向业务 JS/第三方脚本 |
| 样式/布局范围过大 | Performance 的 Recalculate Style/Layout,Rendering overlays | 每次微小改动都出现大范围 layout,读写交错可复现 |
| 绘制/栅格太重 | Performance 的 Paint/Raster,Layers/Rendering | paint record 或 raster 占大头,滚动引入新 tiles 时掉帧 |
| 合成/GPU 是瓶颈 | Frames、Compositor/GPU trace 轨道 | 主线程并不忙,frame submission 或 GPU 工作仍跨帧 |
| 首屏被资源卡住 | Network waterfall、Coverage、LCP 诊断 | CSS/font/关键图像的发现或下载落在首帧/LCP 之前 |
| 点击位置或焦点异常 | Elements、Rendering、Accessibility pane、真实交互 | 可见盒与 hit target/focus order 不一致 |
录制要能复现且保留上下文:设备能力、DPR、刷新率、网络条件、缓存状态、浏览器版本和操作步骤。一次本机 trace 只能证明该条件下的因果链;移动端、低端 GPU 或不同内核可能把瓶颈移到别处。
9. 失败模式与修复方向
| 失败模式 | 机制链 | 先验证,再决定 |
|---|---|---|
| 列表展开掉帧 | 内容增多 -> layout/paint 区域扩大 -> raster 赶不上 | 虚拟化、固定尺寸/渐进加载是否真的缩短 presentation delay |
| 拖拽时卡 | 高频 pointermove -> JS 读写交错 -> forced layout | 合并事件、只在 rAF 写、将可合成位移与测量分离 |
| 滚动首屏空白 | 新视口 tiles 未就绪或主线程阻塞滚动 | 降低单 tile 复杂度、检查监听器与图块优先级证据 |
动画加 will-change 后更差 | 分层增多 -> 纹理/显存/混合负担增大 | Layers 和内存变化;只保留确有收益的短期提示 |
| 文本晚到后页面跳动 | 字体/图片资源解析 -> 尺寸变化 -> layout shift | 显式预留尺寸、评估 font-display 和关键资源优先级 |
10. 学习检验
选一个有滚动列表、悬停动画和图片的页面,在 DevTools 录三段 trace:首次加载、快速滚动、点击展开。对每段写出:
- 这次用户可见的变化来自 style、layout、paint、raster、composite 中的哪些阶段?证据在哪条轨道?
- 哪个中间表示必须更新,哪个可以复用?这是依据而不是猜测。
- 若改为只改变
transform,语义和布局是否仍正确?它是减少工作还是只把成本转到 GPU? - 用真实点击、滚动和键盘焦点验证,视觉优化是否破坏交互边界。
再到 Flutter 对照,比较 Widget/Element/RenderObject/Layer/Scene 如何承担类似职责,以及为何它没有 DOM/CSSOM 却仍面对布局、绘制、栅格和合成的同类约束。
资料与时效边界
- CSS 的盒、格式化与绘制语义以 CSS Display、CSS Fragmentation 和 CSS Painting 等规范及其关联模块为准(访问于 2026-07-29)。
- Chromium/Blink 的帧生命周期与合成职责参考 Life of a Frame 和 cc compositor overview(访问于 2026-07-29)。
- 层、绘制、栅格与交互证据的采集参考 Chrome DevTools Performance 与 Rendering performance(访问于 2026-07-29)。
LayoutObject、fragments、paint chunks、property trees、tiles 与 compositor quads 是 Chromium/Blink 的实现词,不是 Web API。不同 Chromium 版本、GPU、缩放和页面结构都会改变实际分层、失效范围和线程轨迹;本页只提供可验证的职责地图,不承诺固定的内部数据结构或优化结果。