Webkit与重绘重排
重排和重绘是浏览器渲染引擎的工作机制,而 WebKit 正是最著名、历史最悠久的渲染引擎之一,
webkit里真实名称
虽然我们常把“重排”叫 Reflow,但在 WebKit 的源码和核心架构里,它更标准的叫法是 Layout(布局)。
- 重排 (Reflow / Layout):在 WebKit 中,对应的是
RenderObject(渲染对象)树去计算每一个盒子几何位置的过程。WebKit 有一个专门的内核类叫RenderView和RenderBlock来负责执行 Layout 算法。 - 重绘 (Repaint):在 WebKit 中,对应的是
GraphicsContext去重新填充像素的过程。
(注:Reflow 这个词最早是 Firefox 的 Gecko 引擎提出的,后来因为太形象,大家就把它当成了重排的通用代名词。)
在 WebKit 的架构里,一次页面的渲染大致是:DOM 树 -> 样式计算 -> Render 树 -> Layout (重排) -> Paint (重绘) -> Composite (合成)。
- 当 JS 修改了元素的
width时,WebKit 内部的RenderObject会标记自己为needsLayout。 - 在下一帧到来时,WebKit 会调用
layout()方法,重新计算整棵树,这就是重排。 - 重排完成后,WebKit 发现外观变了,会标记
needsPaint,然后调用paint()方法,把像素画到内存的位图里,这就是重绘。
重排和重绘是很消耗性能的,所以webkit以及后来的blink引擎,选择引入了渲染层和图形层概念:
- 如果你只修改了
transform或opacity,WebKit 根本不会触发 Layout 和 Paint,而是直接让 GPU 重新组合已有的图层(Composite)。 - 这就是为什么现代 WebKit 引擎能做到 60fps 丝滑动画的底层秘密——绕开了重排和重绘。
渲染层和图形层:
Renderlayer是浏览器在dom树和渲染树之后建立的逻辑分层结构,负责解决元素在视觉树的堆叠和覆盖问题,让决定元素在z轴上的堆叠顺序,比如z-index,确保透明、重叠元素能够正确显示,并处理页面内容的渲染层级。
文档根节点和就具有明确的css定位比如relative、absolute、transform 、透明、overflow、mask、filter或者包含3d/加速2d canvas及video元素的节点才会创建。renderlayerf不参与最终的gpu渲染,它的树结构是为了下一步的合成做逻辑准备的。
grapicslayer才是renderlayer的物理存储实现,也是真正作为纹理上传给gpu进行合成的层,负责管理实际的后端存储空间,将renderlayer的内容绘制成位图,最终交给合成器compositor进行gpu合成,输出最终的屏幕图像。一个 RenderLayer 可能对应一个或多个 GraphicsLayer(用于管理前景、背景等不同存储),而 GraphicsLayer 则构成了与 RenderLayer 树并行的另一棵树。
它们协同的核心,就是浏览器为了极致性能而构建的硬件加速合成化渲染流程。可以简化为:DOM Tree -> RenderObject Tree -> RenderLayer Tree(逻辑分层)-> GraphicsLayer Tree(物理存储)-> GPU 合成
对于实践来说,我们得迎合渲染引擎,实现极致的高性能渲染,核心是想尽一切办法让CSS的变化停留在合成composite阶段,彻底跳过layout和paint环节。如果说你就用with、height、padding、margin、border、top、left、bottom、right、font-size做动画的话,浏览器的每一帧都在经历重排和重绘。
这种情况下我们会把动画交给gpu处理,webkit引擎在处理transform和opacity的时候会把他们提升为一个独立的合成层,叫做compositing layer:
/* 极致性能:不重排、不重绘,仅在 GPU 合成阶段处理 */
.box {
transform: translateX(100px) scale(1.1);
transition: transform 0.3s ease;
}
如果说一个复杂组件,比如说长列表的其中一项,或者一个复杂的图表,它的内部发生了重排,但是你不喜欢它影响到整个页面,你可以使用contain属性,强制浏览器把该元素以及其子树的布局、绘制与外部隔离:
.card {
/* 告诉引擎:我内部变化不要影响外面,我也尽量不受外面影响 */
contain: layout paint style;
/* 或者直接用最强隔离 */
/* contain: strict; */
}
contain: layout:内部布局变化,绝对不会引发外部元素重排。contain: paint:内部元素超出边界绝对不会画到外面(类似 overflow: hidden 但不创建 BFC)。content-visibility: auto:这是配合contain的大杀器。对于屏幕外(未滚动到可视区域)的 DOM,浏览器直接跳过其渲染树构建、布局和绘制,性能提升极其恐怖。
如果是颜色,背景等必须重绘的内容,我们会尽量减少重绘的面积,用box-shadow来代替外部图片或者大体积元素,避免大面积使用极高模糊度的filter: blur,这个极度消耗gpu性能,尤其是移动端。如果某个元素需要频繁改变样式的话,并且他在屏幕中很小,可以把他的z-index设置为负数或者将其相对定位,避免触发大范围的脏矩形重绘。
由于webkit的布局计算时批量异步处理的,在下一帧执行,因此js操作css容易踩坑,如果你在同一个循环里先修改了样式又立即读取了样式,浏览器为了给你正确的结果,会立即打断异步队列,强制执行重排:
// 极其糟糕的代码:循环内读写交替,导致强制连续重排
for (let i = 0; i < 100; i++) {
el.style.left = el.offsetLeft + 1 + 'px'; // 读(引发重排) -> 写 -> 读(引发重排)...
}
// 读写分离:先集中读,再集中写
const lefts = [];
for (let i = 0; i < 100; i++) lefts.push(el[i].offsetLeft); // 统一读
for (let i = 0; i < 100; i++) el[i].style.left = lefts[i] + 1 + 'px'; // 统一写
优化技巧:
如果说发现某个动画掉帧,你可以认为创建一个单独的渲染层,RenderLayer->GraphicsLayer:
.fast-animation {
will-change: transform; /* 提前告诉引擎:我马上要变了,把层建好 */
/* 或者用 3D 变换欺骗引擎 */
transform: translateZ(0);
}
这个属性绝不能滥用。合成层极其消耗内存,层建得太多,会导致内存爆炸和页面卡顿。只对即将发生动画、或者频繁变化的元素使用。
WebKit 和 Blink 的关系
Google Chrome 早期用的就是 WebKit 引擎,后来 Google 觉得 WebKit 的代码太老了,于是从 WebKit 中 Fork 出了一个分支,命名为 Blink(现在 Edge、Opera 等都在用)。
所以,你在 Chrome 开发者工具(Performance 面板)里看到的绿色 Layout 和紫色 Paint 块,其底层祖传代码和架构理念,正是来源于 WebKit