CSS 动画中的重排与重绘从 Chrome DevTools 到代码优化一、我只改了 opacity为什么 C 页面滚动还是卡你在一个动画性能优化任务中做了所有正确的事——把top改成了transform: translateY()把width改成了transform: scaleX()所有动画都限定在transform和opacity两个 Composite-only 属性上。但 C 页面的滚动仍然在 45fps 附近波动。打开 Chrome DevTools Performance 面板录制——发现一段 8ms 的Recalculate Style后面紧跟着一段 35ms 的Layout。追踪堆栈动画的keyframes是纯transform但动画所在的父容器使用了getComputedStyle通过某个 Chart 库的内部尺寸计算触发导致浏览器强制同步布局Forced Synchronous Layout沿着整个 DOM 树重新计算布局。这就是你优化了动画本身但没有优化动画所处的上下文的典型场景。浏览器的渲染引擎是全局性的——一个角落的同步 Layout 请求会冻结整个页面的布局管道影响到那个角落之外的一切动画。动画性能优化的核心不只是选择 Composite-only 的属性还包括不要让任何代码在动画进行中触碰 Layout-triggering 的 API。二、重排/重绘/合成的触发属性分类flowchart LR A[修改 CSS 属性] -- B{触发的br/渲染阶段} B --|width/heightbr/margin/paddingbr/top/left/right/bottombr/displaybr/border-widthbr/font-sizebr/position| C[Layout (重排)br/耗时: 10-50ms] B --|colorbr/backgroundbr/box-shadowbr/border-colorbr/outline| D[Paint (重绘)br/耗时: 5-20ms] B --|transformbr/opacitybr/filterbr/backdrop-filter| E[Composite (合成)br/耗时: 1-3ms] C -- F[波及范围: 整个文档流br/后续元素全部重新布局] D -- G[波及范围: 当前元素 br/重叠元素的重绘区域] E -- H[波及范围: 仅当前合成层br/对其他层无影响]三、从 DevTools 到代码的优化路径使用 Performance 面板定位 FSL/** * 强制同步布局Forced Synchronous Layout检测工具 * * FSL 发生的模式 * 1. JavaScript 写入 DOM修改布局属性 * 2. JavaScript 立即读取 DOMoffsetWidth、getBoundingClientRect 等 * 3. 浏览器必须在读取前完成布局计算 → 强制同步 Layout * * 在 Chrome DevTools 中 * Performance 面板 → 勾选 Enable advanced paint instrumentation * → 寻找标有 Forced reflow 的 Layout 事件 * → 点击展开查看触发该 FSL 的 JavaScript 调用栈 */ const FSL_TRIGGERING_PROPERTIES [ offsetTop, offsetLeft, offsetWidth, offsetHeight, scrollTop, scrollLeft, scrollWidth, scrollHeight, clientTop, clientLeft, clientWidth, clientHeight, getClientRects, getBoundingClientRect, getComputedStyle, innerText, ]; /** * 安全的批量 DOM 读写模式 * 将写入和读取分阶段执行避免写-读交叉触发 FSL */ class DOMBatchReadWrite { private reads: Array() void []; private writes: Array() void []; /** 注册一个读取操作 */ read(fn: () void) { this.reads.push(fn); return this; } /** 注册一个写入操作 */ write(fn: () void) { this.writes.push(fn); return this; } /** 执行先全部读 → 再全部写 */ execute() { // 阶段 1所有读取操作基于上一帧的布局结果 this.reads.forEach((fn) fn()); // 阶段 2所有写入操作触发一次 Layout this.writes.forEach((fn) fn()); // 阶段 3清理下一帧读取会基于这帧的写入结果 this.reads []; this.writes []; } } // ❌ FSL 示例读-写-读交叉 // const el document.querySelector(.box); // el.style.width 200px; // 写 // const w el.offsetWidth; // 读 → 强制 Layout // el.style.height w px; // 写 // ✅ 正确的批处理模式 // const batch new DOMBatchReadWrite(); // const rect el.getBoundingClientRect(); // 读基于当前布局 // batch.write(() { // el.style.width 200px; // 写 // el.style.height rect.width px; // 基于之前读取的值 // }); // batch.execute();优化 Paint 阶段/** * Paint 优化实践 * * 原则 * 1. 将频繁重绘的元素提升为独立合成层 * 2. 减少重绘面积clip-path、border-radius 限制范围 * 3. 使用 opacity/transform 动画代替 color/background 动画 */ /* ❌ 每帧都触发 Paint */ .bad-animation { animation: bg-pulse 2s infinite; } keyframes bg-pulse { 0%, 100% { background-color: #3B82F6; } 50% { background-color: #60A5FA; } /* 颜色变化 → Paint */ } /* ✅ 仅在 Composite 阶段执行 */ .good-animation { position: relative; background-color: #3B82F6; /* 用 ::after 的 opacity 变化模拟颜色渐变 */ } .good-animation::after { content: ; position: absolute; inset: 0; background-color: #60A5FA; opacity: 0; animation: opacity-pulse 2s infinite; } keyframes opacity-pulse { 0%, 100% { opacity: 0; } 50% { opacity: 1; } /* opacity → Composite only */ } /* 限制重绘范围用 will-change 创建合成层 */ .frequent-repaint { /* 将这个元素从父元素的绘制区域中隔离出来 */ will-change: transform; transform: translateZ(0); }Chrome DevTools Rendering 面板的实战检查调试步骤 1. DevTools → More tools → Rendering 2. 勾选 Paint flashing → 页面重绘区域以绿色闪烁标记 3. 如果动画运行时大面积区域闪烁绿色 → Paint 风暴 4. 勾选 Layer borders → 橙色边框 合成层蓝色网格 分块 5. 勾选 FPS meter → 实时监控帧率 6. Layers 面板 → 查看每个合成层的纹理尺寸和内存占用 判断标准 - 绿色闪烁区域不超过动画元素自身的边界 → Paint 健康 - 橙色合成层边框 ≤ 20 个移动端→ Composite 健康 - FPS 始终 ≥ 55 → 帧率健康四、优化的反面教材过度合成层化。看到重绘闪烁就给每个元素will-change: transform——结果创建了 80 个合成层GPU 显存爆炸Composite 阶段从 2ms 增加到 15ms。应该在重绘开销 合成层开销时才创建合成层。通用规则如果元素面积 视口的 10% 且每帧都在重绘创建合成层如果元素面积 视口的 5%重绘的成本可能低于合成层的纹理管理开销。contain属性的误用。contain: layout style paint可以限制该元素内部的 Layout/Paint 不会扩散到外部——这在理论上可以阻止子元素的动画导致全局 Layout。但contain: paint会创建一个新的合成层边界如果滥用效果等同于大量will-change。五、总结触发布局的属性width/height/margin/top/left修改影响整个文档流——波及后续所有元素的重排。触发重绘的属性color/background/box-shadow修改只影响当前元素——但重绘面积可能覆盖更多区域。仅触发合成的属性transform/opacity在 GPU 合成器上执行——动画性能的最优选择。强制同步布局FSL JS 写入布局属性 → 立即读取 → 浏览器被迫在写-读之间插入同步 Layout。FastDom 模式先全部读、再全部写是消除 FSL 的工程方案——将 rAF 回调中的读写分离。Paint flashingRendering 面板用绿色闪烁直观展示重绘区域——动画性能调试的X 光。opacity 叠加变色可以替代 background-color 动画——用纯 Composite 实现颜色渐变效果。will-change不应用在 视口 5% 面积的小元素上——合成层的纹理管理开销可能 重绘成本。Layer borders橙色/蓝色网格和 Layers 面板可显示所有合成层和纹理内存——排查合成层过载的核心工具。动画性能优化的顺序先消除 Layout → 再消除 Paint → 最后优化 Composite 层数。