公众号:小博的前端笔记
一:重绘和回流
重绘(Repaint)和回流(Reflow)是浏览器渲染引擎在更新网页显示时的两个核心过程,它们对网页性能有重大影响:
-
回流 (Reflow / Layout):
-
定义: 当渲染树(Render Tree)中的一部分或全部因为元素的尺寸、结构、布局或某些属性(影响几何位置)发生改变,浏览器需要重新计算所有受影响元素的几何属性(位置、大小)的过程。
-
触发条件: 任何改变元素几何信息的操作都会触发回流。常见触发点:
- 添加、删除、移动可见的 DOM 元素。
- 元素尺寸改变(宽度、高度、内边距、边框、外边距)。
- 内容变化(文本数量改变、图片大小改变等)。
- 浏览器窗口大小改变(
resize事件)。 - 激活 CSS 伪类(如
:hover)。 - 计算
offsetWidth,offsetHeight,clientWidth,clientHeight,scrollTop等布局属性(因为浏览器需要提供最新值,会强制触发同步回流)。 - 设置
style属性值(尤其影响几何布局的属性)。 - 改变元素的
display属性(如none到block)。 - 修改影响布局的 CSS 样式(如
width,height,position,float,font-size,line-height,text-align,overflow等)。
-
-
重绘 (Repaint):
-
定义: 当元素的外观、样式(如颜色、背景色、边框颜色、可见性等)发生改变,但不影响其布局(几何位置和大小不变)时,浏览器需要重新绘制受影响区域到屏幕上的过程。
-
触发条件: 改变不影响元素在文档流中位置的样式属性。常见触发点:
- 改变
color、background-color、border-color、outline-color、visibility(非display: none)等。 - 改变
text-decoration。 - 改变
background-image。 - 改变
box-shadow。
- 改变
-
关键关系:
- 回流必然导致重绘。 如果一个元素的位置或尺寸变了,浏览器肯定需要重新绘制它(以及可能受其影响的其他元素)。
- 重绘不一定需要回流。 如果只是颜色、背景等不影响布局的属性改变,浏览器只需重绘,不需要重新计算布局。
为什么需要减少重绘和回流? 它们都是计算密集型操作,尤其是在复杂文档(DOM 树很大)或设备性能有限(如移动端)的情况下:
- 性能开销大: 回流需要遍历渲染树重新计算几何信息,重绘需要重新填充像素。频繁触发会导致 CPU 和 GPU 负载升高。
- 阻塞渲染: JavaScript 执行和页面渲染通常共享一个主线程。频繁的回流/重绘会占用主线程时间,导致页面卡顿、响应迟缓,用户体验变差(帧率下降)。
二、如何减少重绘和回流(性能优化):
-
避免频繁操作样式:
- 合并多次样式修改: 不要一条条地修改
element.style属性。尽量一次性修改style.cssText或直接切换预定义的 CSS 类 (element.className或element.classList)。
// 不好:触发多次回流/重绘 el.style.marginLeft = '10px'; el.style.marginTop = '20px'; el.style.width = '100px'; // 好:合并为一次修改 (使用 cssText 或 类) el.style.cssText += '; margin-left: 10px; margin-top: 20px; width: 100px;'; // 或 el.classList.add('new-styles'); // .new-styles 类里定义所有需要的样式 - 合并多次样式修改: 不要一条条地修改
-
批量修改 DOM:
- 使用
DocumentFragment: 在内存中构建一个 DOM 片段,完成所有修改后一次性添加到真实 DOM 中。只触发一次回流。 - 克隆修改: 克隆一个节点,在副本上修改,然后用副本替换原节点。
- 隐藏元素再修改 (
display: none): 将元素暂时移出文档流 (display: none),进行一系列修改(此时修改不会触发回流),然后再显示它 (display: block等)。触发两次回流(隐藏和显示时)。
- 使用
-
避免触发同步布局(强制同步回流):
- 读写分离: 避免在修改样式后立即读取布局属性(如
offsetTop,scrollHeight,getComputedStyle())。浏览器为了给你最新的值,会强制刷新渲染队列,导致立即执行一次回流。 - 先读后写: 如果需要读取布局属性,最好在修改样式之前一次性读取完所有需要的值,或者将读取操作和写入操作分开(用
requestAnimationFrame延迟写入)。
// 不好:强制同步回流 (布局抖动) for (let i = 0; i < items.length; i++) { items[i].style.width = (baseWidth + offset) + 'px'; // 写 let newHeight = container.offsetHeight; // 读 (强制回流获取最新高度) // ... 可能用 newHeight 做其他事 ... } // 好:先读取所有值 (如果可能) let containerHeight = container.offsetHeight; // 读一次 for (let i = 0; i < items.length; i++) { items[i].style.width = (baseWidth + offset) + 'px'; // 写 // 使用之前读取的 containerHeight } // 更好:使用 requestAnimationFrame 分离读写 (现代最佳实践) function updateSizes() { // 在下一帧开始时一次性写入 for (let i = 0; i < items.length; i++) { items[i].style.width = calculateWidth(i) + 'px'; } } // 可能先读取一些需要的值... requestAnimationFrame(updateSizes); - 读写分离: 避免在修改样式后立即读取布局属性(如
-
优化 CSS:
- 避免使用 CSS 表达式 (
calc()要谨慎): 虽然现代calc()优化较好,但过度复杂或频繁变化的计算仍可能影响性能。 - 避免深层级或复杂的选择器: 选择器匹配效率会影响渲染树构建。
- 谨慎使用
table布局: 即使很小的改动也可能导致整个表格回流。尽量用div和现代布局技术 (Flexbox, Grid)。 - 使用
transform和opacity实现动画: 这两个属性通常由合成器线程处理,不会触发主线程的回流和重绘(只触发合成 Composite),是实现高性能动画的首选。 - 使用
will-change属性 (谨慎): 提前告知浏览器哪些元素将要变化 (transform,opacity等),让浏览器提前做好优化准备(如创建独立图层)。但不要滥用,过度使用会增加内存消耗。
- 避免使用 CSS 表达式 (
-
元素位置优化:
- 使用
position: absolute或position: fixed: 将动画元素或频繁变化的元素脱离普通文档流。这样它们的回流只会影响自身及其子元素(影响范围小),不会引起整个页面的大规模回流。但仍需谨慎使用top/left等属性,优先用transform。
- 使用
-
利用浏览器优化:
- 现代浏览器会将多次回流操作放入队列,然后在合适的时机(如一定时间间隔、代码执行间隙)批量执行一次回流。但如前所述,读取布局属性会强制刷新这个队列,导致立即执行回流。
总结关键策略:
- 合并操作: 无论是样式修改还是 DOM 操作,尽可能合并成单次操作。
- 脱离文档流: 对频繁变动的元素使用定位 (
absolute/fixed) 或display: none策略。 - 读写分离: 绝对避免在循环或高频事件中交叉读写布局属性。使用
requestAnimationFrame安排写入。 - 善用高性能属性: 动画优先用
transform和opacity。 - 优化 CSS 结构和选择器。
通过应用这些策略,可以显著减少不必要的重绘和回流,从而提升网页的渲染性能和用户体验。在 Chrome DevTools 的 Performance 面板中记录运行时性能,可以直观地看到 Layout (回流) 和 Paint (重绘) 事件及其耗时,是诊断和优化的必备工具。
三、什么是合成 Composite
1. 什么是合成(Composite)?
合成是浏览器渲染流水线(Pixel Pipeline)的最后一步。它负责将页面的不同部分(通常是存储在内存中的位图,称为图层 Layers 或 合成层 Compositing Layers)按照正确的顺序(考虑 z-index、重叠等)和位置组合(拼合)起来,最终形成用户在屏幕上看到的完整一帧画面。
你可以把合成想象成:
- 舞台表演: 将页面想象成一个舞台。每个图层就像一张透明的醋酸纤维片(Celluloid Sheet),上面画着舞台的一部分(比如背景、一个动画角色、一个静态按钮)。合成就是把这些透明的“片”按照导演(渲染树)的指示,从上到下(或考虑深度)叠加在一起,最终形成观众(用户)看到的完整场景画面。
- Photoshop 图层: 类似于在 Photoshop 中,你有多个图层(背景层、文字层、图片层)。最终显示的图片是所有可见图层叠加合成后的结果。浏览器合成就是做类似的事情。
2. 合成是如何工作的?(关键概念)
-
分层(Layering): 浏览器并不是把整个页面当作一个巨大的位图来处理。它会根据 CSS 属性(如
transform,opacity,will-change,position: fixed等)和内容(如<video>,<canvas>)等因素,将页面的不同部分提升(Promote) 为独立的合成层(Compositing Layer) 。这些层被单独存储在 GPU 的内存中(作为纹理 Texture)。 -
光栅化(Rasterization): 对于每个合成层,浏览器需要将其内容(HTML/CSS)转换成实际的像素点(位图)。这个过程可以在主线程进行,但现代浏览器更倾向于使用光栅线程(Raster Threads) (通常是合成线程的一部分)在 GPU 上进行快速光栅化,效率更高。
-
合成线程(Compositor Thread): 浏览器有一个独立的合成线程(Compositor Thread) 。它的核心职责就是处理合成工作:
- 管理图层: 维护所有合成层的列表及其属性(位置、透明度、变换矩阵等)。
- 创建合成帧: 根据图层信息和它们的层次关系(层叠上下文),计算每个图层在最终屏幕上的确切像素位置。
- 发送指令给 GPU: 将计算好的图层信息和绘制指令(“在 (x, y) 位置绘制图层 A 的纹理,应用透明度 0.8,然后在其上绘制图层 B...”))提交给 GPU。
-
GPU 绘制: GPU 接收到合成线程的指令后,利用其强大的并行处理能力,非常高效地将所有图层的纹理绘制(合成)到屏幕的帧缓冲区(Frame Buffer)中,形成最终用户看到的画面。
3. 为什么合成性能高?(核心优势)
-
避开主线程和回流/重绘: 这是合成的最大优势!当元素的改变只影响合成属性(主要是
transform和opacity)时:-
这些属性的改变通常在 JavaScript 执行或样式计算阶段就处理完了。
-
不需要触发布局(回流/Layout)或绘制(重绘/Paint)! 因为:
transform(如translate,rotate,scale) 改变的是元素在合成层空间内的位置、旋转或缩放,它不影响元素自身及其周围元素在文档流中的几何位置(不会导致回流)。它改变的只是这个层在最终合成时如何被“摆放”。opacity改变的是整个图层的透明度,在合成阶段应用即可,不需要重新绘制图层内部内容(只要图层内容本身没变)。
-
合成线程可以直接利用 GPU 中已有的图层纹理,只更新该图层的
transform矩阵或opacity值,然后告诉 GPU 重新合成。这个过程完全绕过了耗时的回流和重绘步骤,并且发生在独立的合成线程和 GPU 上,不会阻塞主线程执行 JavaScript、处理事件或进行其他渲染工作。
-
-
GPU 加速: GPU 专为并行处理图形计算(如纹理映射、变换、混合/合成)而设计,执行合成操作的速度极快。
-
增量更新: 如果页面的某个部分(一个合成层)发生了变化,浏览器通常只需要重新光栅化并更新那个特定的层,然后在合成时将其与其他未变化的层重新组合即可,无需重绘整个页面。
4. 哪些样式属性可以触发合成(避免重绘/回流)?
核心属性是:
-
transform: 这是实现高性能动画的利器。常用值:translate(x, y)/translate3d(x, y, z)(移动)scale(x, y)(缩放)rotate(angle)(旋转)skew(x-angle, y-angle)(倾斜)matrix()/matrix3d()(矩阵变换 - 最强大但也最复杂)- 关键: 使用
translate3d(0, 0, 0)或translateZ(0)有时可以“欺骗”浏览器提前将元素提升为合成层(利用 3D 变换特性),即使没有实际 3D 变换。
-
opacity: 改变元素的透明度。 -
will-change: 谨慎使用! 这是一个提示(Hint)属性,告诉浏览器你预期某个元素将要如何改变(如will-change: transform, opacity;)。浏览器可以据此提前将该元素提升为独立的合成层,并分配 GPU 资源,为即将到来的变化(特别是transform/opacity变化)做准备,使后续的变化更加流畅。但滥用will-change(如设置大量元素或设置不需要的属性)会导致不必要的内存开销(每个合成层都需要 GPU 内存)和层管理开销(“层爆炸” Layer Explosion),反而可能损害性能。只在需要时对真正会频繁动画的元素使用。 -
其他可能触发分层的属性:
filter(某些滤镜如blur在现代浏览器也可能高效合成),backdrop-filter,position: fixed/sticky(通常需要独立层处理滚动),<video>,<canvas>, WebGL, 有 3D 变换 (perspective,transform-style: preserve-3d) 的子孙元素等。但这些属性本身的变化不一定能像transform/opacity那样完全避免重绘。
5. 如何利用合成进行优化?(最佳实践)
-
动画首选
transform和opacity: 对于移动、缩放、旋转、淡入淡出效果,绝对优先使用 CSStransform和opacity属性,结合 CSS Transitions 或 CSS Animations / Web Animations API。这能确保动画运行在合成线程和 GPU 上,极其流畅。- 避免使用:
top/left/bottom/right,margin/padding,width/height等会触发回流的属性做动画。使用transform: translate()代替top/left移动元素。
- 避免使用:
-
谨慎使用
will-change:- 只对你明确知道会频繁进行
transform或opacity动画的元素使用。 - 在动画开始前稍早设置(例如在触发事件处理程序中),并在动画结束后移除(
elem.style.willChange = 'auto'或覆盖掉)以释放资源。 - 避免应用于过多元素或大面积元素。
- 指定具体的属性(如
will-change: transform, opacity;),避免使用will-change: all;。
- 只对你明确知道会频繁进行
-
注意层创建的成本: 虽然合成性能高,但创建和管理合成层本身(分配 GPU 内存、通信开销)也有成本。过多的、不必要的合成层(“层爆炸”)会消耗大量内存,尤其在低端设备上,反而可能导致卡顿甚至崩溃。使用浏览器开发者工具(如 Chrome DevTools 的 Layers 面板)检查页面分层情况,识别并优化过度分层。
-
注意
filter和backdrop-filter: 虽然它们可能被合成,但一些复杂滤镜(特别是影响大面积区域的模糊)仍然可能很耗性能。测试其在实际设备上的表现。
总结:
-
合成(Composite) 是将预先光栅化好的图层(合成层)拼合成最终屏幕画面的过程,发生在独立的合成线程和GPU上。
-
核心优势: 当改变仅限于
transform和opacity属性时,可以完全跳过回流(Layout)和重绘(Paint) 这两个最耗性能的步骤,实现极其流畅的动画和交互。 -
优化关键:
- 动画/高频交互使用
transform(尤其是translate,scale,rotate) 和opacity。 - 谨慎使用
will-change提示浏览器提前优化。 - 监控图层数量,避免“层爆炸”。
- 动画/高频交互使用
-
工具: 利用 Chrome DevTools 的 Performance 面板分析渲染性能,Rendering 面板中的 Paint flashing 查看重绘区域,Layers 面板查看分层情况和内存占用。