Skip to content

浏览器内核渲染细节:从失效到 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

以下术语来自 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 quads

LayoutObject 与 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 架构。

布局失效

布局依赖可用空间和相邻/祖先几何。改变 widthfont-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 layerCSS 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/heightaspect-ratio 决定资源到达后是否造成布局位移。

资源优先级是浏览器启发式与提示共同决定的结果。preloadfetchpriority 等只能用于已证明处于关键路径的资源;预加载过多会抢占真正关键资源。用 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/Renderingpaint 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:首次加载、快速滚动、点击展开。对每段写出:

  1. 这次用户可见的变化来自 style、layout、paint、raster、composite 中的哪些阶段?证据在哪条轨道?
  2. 哪个中间表示必须更新,哪个可以复用?这是依据而不是猜测。
  3. 若改为只改变 transform,语义和布局是否仍正确?它是减少工作还是只把成本转到 GPU?
  4. 用真实点击、滚动和键盘焦点验证,视觉优化是否破坏交互边界。

再到 Flutter 对照,比较 Widget/Element/RenderObject/Layer/Scene 如何承担类似职责,以及为何它没有 DOM/CSSOM 却仍面对布局、绘制、栅格和合成的同类约束。

资料与时效边界

LayoutObject、fragments、paint chunks、property trees、tiles 与 compositor quads 是 Chromium/Blink 的实现词,不是 Web API。不同 Chromium 版本、GPU、缩放和页面结构都会改变实际分层、失效范围和线程轨迹;本页只提供可验证的职责地图,不承诺固定的内部数据结构或优化结果。

学习规划与研究资料库