回流和重绘

4 阅读6分钟

回流(Reflow)和重绘(Repaint)  是浏览器渲染网页过程中两个关键的性能相关概念,理解它们有助于优化前端性能。


一、基本概念

1. 回流(Reflow / Layout)

  • 定义:当 DOM 元素的几何属性(如宽高、位置、边距等)发生变化时,浏览器需要重新计算元素的布局(即“布局树”或“盒模型”),这个过程称为回流

  • 触发条件(常见):

    • 改变元素的 widthheightpaddingmarginborder 等尺寸属性
    • 添加/删除可见的 DOM 元素
    • 改变字体大小(font-size
    • 用户行为(如窗口缩放、滚动)
    • 调用某些 JavaScript 属性(如 offsetWidthclientHeightgetComputedStyle() 等)会强制触发回流(称为“强制同步布局”)

回流会影响后续所有元素的布局,甚至可能影响整个页面,因此开销较大。

2. 重绘(Repaint / Redraw)

  • 定义:当元素的外观(如颜色、背景、边框样式等)发生变化,但不影响布局时,浏览器只需重新绘制该元素的视觉表现,这个过程称为重绘

  • 触发条件(常见):

    • 改变 colorbackground-colorvisibility(注意:visibility: hidden 不触发回流,但会重绘)
    • 修改 outlinebox-shadow 等不影响布局的样式

重绘不一定触发回流,但回流一定会触发重绘(因为布局变了,画面自然要更新)。


二、性能影响

  • 回流 > 重绘 > 普通渲染(性能开销依次降低)
  • 频繁的回流/重绘会导致页面卡顿、掉帧,影响用户体验。

三、优化策略

✅ 减少回流和重绘的方法:

  1. 批量修改 DOM 或样式

    js
    // ❌ 错误:多次触发回流
    el.style.width = '100px';
    el.style.height = '100px';
    el.style.color = 'red';
    
    // ✅ 正确:合并为一次操作
    el.style.cssText = 'width: 100px; height: 100px; color: red;';
    // 或使用 class 切换
    el.className = 'new-style';
    
  2. 避免频繁读取会触发回流的属性

    js
    // ❌ 每次读取 offsetWidth 都可能触发回流
    for (let i = 0; i < 100; i++) {
      el.style.left = el.offsetWidth + 10 + 'px';
    }
    
    // ✅ 先缓存值
    const width = el.offsetWidth;
    for (let i = 0; i < 100; i++) {
      el.style.left = width + 10 + 'px';
    }
    
  3. 使用文档片段(DocumentFragment)操作大量 DOM

    js
    const fragment = document.createDocumentFragment();
    for (let i = 0; i < 1000; i++) {
      const li = document.createElement('li');
      li.textContent = i;
      fragment.appendChild(li);
    }
    list.appendChild(fragment); // 只触发一次回流
    
  4. 将频繁变化的元素脱离文档流

    • 使用 position: absolute 或 fixed,使其不影响其他元素布局。
    • 动画元素尽量使用 transform 和 opacity(这些属性由合成器处理,不触发回流/重绘)。
  5. 使用 CSS 动画代替 JS 动画

    • 优先使用 transformopacity 实现动画,它们可以被 GPU 加速,且不会触发回流。
  6. 避免使用 table 布局

    • 表格中一个单元格变化可能导致整个表格重新计算(回流代价高)。

四、现代浏览器的优化

  • 浏览器会自动合并多个回流/重绘操作(如在同一个事件循环中),延迟执行以提升性能。
  • 强制同步布局(如读取 offsetWidth 后立即写样式)会打断这种优化,导致“布局抖动”(Layout Thrashing)。

总结

操作触发
width/height/padding/margin 改变✅ 回流 + 重绘
添加/删除可见 DOM 元素✅ 回流 + 重绘
font-size 改变✅ 回流 + 重绘
color/background-color 改变❌ 回流,✅ 重绘
visibility: hidden❌ 回流,✅ 重绘
display: none✅ 回流 + 重绘
transform/opacity 改变❌ 回流,❌ 重绘(由合成器处理)
读取 offsetWidthclientHeight 等⚠️ 可能强制同步回流

💡 最佳实践:尽量减少 DOM 操作,使用 CSS 类控制样式,动画使用 transform 和 opacity,避免强制同步布局。

其他QS

1. 为什么读取 offsetWidth 会触发回流?

✅ 深入原理:

  • 浏览器为了返回准确的布局值(如 offsetWidth),必须立即执行所有待处理的回流任务,这称为 强制同步布局(Forced Synchronous Layout)
  • 如果在循环中反复读写,会导致 布局抖动(Layout Thrashing) ,性能极差。
js
// ❌ 危险代码:每次循环都强制回流
for (let i = 0; i < 100; i++) {
  el.style.width = el.offsetWidth + 10 + 'px'; // 读 + 写 → 强制回流
}

💡 高分点:提到“布局抖动”术语,并给出解决方案(先缓存 offsetWidth)。


2. 现代浏览器如何优化回流/重绘?

✅ 加分回答:

  • 浏览器会将多个 DOM 修改合并成一次回流(队列化,延迟执行)。
  • 强制同步操作(如读取布局属性)会打断优化。
  • Chrome DevTools 的 Performance 面板 可以录制并分析回流/重绘耗时。

💡 高分点:提到 DevTools 工具,体现工程实践能力。

3. visibility: hidden 和 display: none的区别?

特性visibility: hiddendisplay: none
是否占据空间✅ 占据原有布局空间(“隐身但占位”)❌ 完全从文档流中移除(不占位)
是否触发回流(Reflow)❌ 不触发回流✅ 触发回流(因为布局改变)
是否触发重绘(Repaint)✅ 触发重绘(元素不可见但需清除像素)✅ 触发重绘(连同子元素一起消失)
子元素是否可见❌ 子元素默认也隐藏(但可通过 visibility: visible 覆盖)❌ 子元素完全不可见且无法显示
是否响应事件❌ 不响应鼠标/键盘事件❌ 不响应事件
是否影响可访问性(如屏幕阅读器)⚠️ 通常仍会被读出(取决于浏览器)✅ 完全忽略,不会被读出

4. opacity

opacity(注意正确拼写是 opacity,不是 "opcity")在现代浏览器中具有一项重要的性能优化机制它可以通过 GPU 加速进行合成(Compositing),而不会触发回流(Reflow)或重绘(Repaint) 。这是前端性能优化中的关键知识点。


✅ 一、opacity 的特殊优化原理

1. 不触发回流 & 重绘

  • 修改 opacity 不会改变元素的几何属性(如宽高、位置),因此:

    • ❌ 不触发 回流(Reflow)
    • ❌ 不触发 重绘(Repaint)

2. 触发“合成”(Compositing)

  • 当 opacity 值 小于 1(如 opacity: 0.9)时,浏览器会将该元素提升为一个独立的合成层(Composited Layer)
  • 后续的 opacity 动画由 GPU 直接处理,通过调整图层的透明度来实现视觉变化,无需重新绘制像素或计算布局。
  • 这个过程称为  “合成”(Compositing) ,发生在渲染流程的最后一步。

📌 渲染流程简化:
Style → Layout(回流) → Paint(重绘) → Composite(合成)
opacity 只影响 Composite 阶段。

二、为什么 opacity 能被 GPU 加速?

  • 浏览器(如 Chrome)会为满足特定条件的元素创建  “合成层”(Compositing Layer) ,例如:

    • 使用 transform
    • 使用 opacity < 1
    • 使用 will-change: opacity
    • 有 videocanvasiframe 等
  • 一旦成为合成层,该元素会被上传到 GPU 纹理(Texture) ,后续动画由 GPU 直接操作纹理的透明度,效率极高。


三、注意事项

  1. opacity: 1 不会创建合成层
    只有 opacity < 1 时才会触发合成优化。如果初始就是 1,动画开始时才变小,可能首帧仍有开销。
  2. 过度使用合成层会导致“层爆炸”(Layer Explosion)
    每个合成层都占用 GPU 内存,过多会导致内存压力。可通过 will-change 或 transform: translateZ(0) 显式提升,但要谨慎。
  3. 子元素继承透明度
    opacity 会影响整个元素及其所有子元素的透明度,无法单独控制子元素不透明(此时可考虑 rgba 背景色)。