返回首页

怎么理解回流跟重绘?什么场景下会触发?

问题解析

回流(Reflow)和重绘(Repaint)是浏览器渲染机制的核心概念,直接影响页面性能。面试考察这个问题,是想要了解候选人对浏览器工作原理的理解,以及能否写出高性能的 CSS。

核心概念

渲染流程

HTML → DOM Tree
  ↓
CSS → CSSOM Tree
  ↓
Render Tree(渲染树)
  ↓
Layout(布局/回流)→ 计算几何信息
  ↓
Paint(绘制/重绘)→ 绘制像素
  ↓
Composite(合成)→ 显示到屏幕

什么是回流(Reflow)

回流(也叫重排)是当渲染树中的元素尺寸、结构或位置发生变化时,浏览器重新计算元素几何属性的过程。

触发回流后

  • 受影响的部分需要重新布局
  • 通常会导致后续元素的位置变化
  • 一定会触发重绘

什么是重绘(Repaint)

重绘是当元素的外观(颜色、背景等)发生变化但不影响布局时,浏览器重新绘制元素的过程。

触发重绘后

  • 仅重新绘制视觉表现
  • 布局不会改变
  • 性能开销小于回流

合成(Composite)

现代浏览器使用 GPU 加速的合成阶段:

  • 将页面分层,独立绘制
  • 通过 GPU 合成最终图像
  • transformopacity 的修改只触发合成

触发场景

触发回流的操作

1. 改变几何属性

/* 这些属性的修改会触发回流 */
width, height
padding, margin
border
position, top, left, right, bottom
float, clear
display
text-align
overflow
font-size, line-height(影响布局时)
vertical-align
white-space

2. DOM 操作

// 添加/删除元素
document.body.appendChild(element);
element.remove();

// 改变内容
element.innerHTML = '新内容';
element.textContent = '新文本';

// 修改类名
element.className = 'new-class';
element.classList.add('active');

3. 查询布局信息(强制回流)

// 这些查询会强制浏览器立即执行回流
const width = element.offsetWidth;
const height = element.offsetHeight;
const top = element.offsetTop;
const left = element.offsetLeft;
const clientWidth = element.clientWidth;
const scrollHeight = element.scrollHeight;
const rect = element.getBoundingClientRect();
const computedStyle = getComputedStyle(element);

⚠️ 重要:在修改样式后立即查询布局信息,会强制浏览器提前执行回流。

4. 其他触发

// 窗口大小变化
window.addEventListener('resize', () => {});

// 字体变化
document.body.style.fontFamily = 'Arial';

// 滚动(部分浏览器)
window.scrollTo(0, 100);

触发重绘的操作

/* 只会触发重绘的属性 */
color
background-color
background-image
border-color
border-radius
box-shadow
text-decoration
outline
visibility

深入理解

回流的影响范围

┌─────────────────────────────────┐
│  Body                           │
│  ┌─────────────┐ ┌───────────┐ │
│  │ Container   │ │ Sidebar   │ │
│  │ ┌─────────┐ │ │           │ │
│  │ │ Element │ │ │           │ │  ← 修改这个元素
│  │ │  🔄     │ │ │           │ │    只影响 Container
│  │ └─────────┘ │ │           │ │    和内部元素
│  └─────────────┘ └───────────┘ │
└─────────────────────────────────┘

回流的影响:

  • 全局回流:修改 html 元素、窗口大小变化
  • 局部回流:修改普通元素,只影响其后代和兄弟元素

强制同步布局(Forced Synchronous Layout)

// ❌ 错误:读写交替,强制回流
function badExample() {
  const boxes = document.querySelectorAll('.box');

  for (let i = 0; i < boxes.length; i++) {
    // 读取(如果没有缓存,会强制回流)
    const width = boxes[i].offsetWidth;

    // 写入
    boxes[i].style.width = (width + 10) + 'px';
  }
  // 每次循环都可能触发回流!
}

// ✅ 正确:先读后写,批量操作
function goodExample() {
  const boxes = document.querySelectorAll('.box');
  const widths = [];

  // 先批量读取
  for (let i = 0; i < boxes.length; i++) {
    widths.push(boxes[i].offsetWidth);
  }

  // 再批量写入
  for (let i = 0; i < boxes.length; i++) {
    boxes[i].style.width = (widths[i] + 10) + 'px';
  }
  // 只触发一次回流
}

布局抖动(Layout Thrashing)

快速连续的读写操作导致浏览器反复回流:

// 极其糟糕的做法
function thrashing() {
  for (let i = 0; i < 1000; i++) {
    const height = element.offsetHeight;  // 读
    element.style.height = (height + 1) + 'px';  // 写
  }
  // 触发 1000 次回流!
}

性能优化

1. 使用 transform 替代位置属性

/* ❌ 触发回流 */
.element {
  position: relative;
  left: 100px;
  top: 100px;
}

/* ✅ 只触发合成 */
.element {
  transform: translate(100px, 100px);
}

2. 使用 opacity 替代 visibility/display(动画场景)

/* 动画时使用 opacity */
.fade-in {
  opacity: 0;
  transition: opacity 0.3s;
}

.fade-in.active {
  opacity: 1;
}

3. 批量 DOM 操作

// ❌ 逐个添加
list.forEach(item => {
  container.appendChild(createElement(item));
});

// ✅ 使用 DocumentFragment
const fragment = document.createDocumentFragment();
list.forEach(item => {
  fragment.appendChild(createElement(item));
});
container.appendChild(fragment);

// ✅ 或先隐藏再操作
container.style.display = 'none';
// ... 批量操作
container.style.display = 'block';

4. 避免强制同步布局

// ❌ 读写交替
const height = element.offsetHeight;  // 读
element.style.height = (height * 2) + 'px';  // 写
const newHeight = element.offsetHeight;  // 又读!强制回流

// ✅ 分离读写
const height = element.offsetHeight;  // 读
const newHeight = height * 2;  // 计算
element.style.height = newHeight + 'px';  // 写

5. 使用 CSS 类批量修改样式

/* ❌ 逐个修改属性 */
element.style.width = '100px';
element.style.height = '100px';
element.style.background = 'red';

/* ✅ 使用类名 */
.expanded {
  width: 100px;
  height: 100px;
  background: red;
}

element.classList.add('expanded');

6. 使用 requestAnimationFrame

// 批量更新安排在下一帧
requestAnimationFrame(() => {
  // DOM 更新操作
});

7. CSS 优化

/* 使用 will-change 提示浏览器 */
.animating-element {
  will-change: transform, opacity;
}

/* 动画结束后移除 */
.animating-element.animation-complete {
  will-change: auto;
}

/* 使用 contain 限制影响范围 */
.isolated-component {
  contain: layout style paint;
}

浏览器优化策略

队列化修改

浏览器会将修改操作放入队列,批量执行:

// 这些修改会被批量处理,可能只触发一次回流
element.style.width = '100px';
element.style.height = '100px';
element.style.margin = '10px';

// 但查询会强制提前执行回流
console.log(element.offsetWidth);  // 强制回流!

分层渲染

浏览器自动将某些元素提升为独立图层:

  • transform: translateZ(0)
  • will-change: transform
  • opacity 动画
  • position: fixed
  • video, canvas, iframe

面试要点

  1. 能够清晰解释回流和重绘的区别
  2. 能够列举触发回流和重绘的常见属性
  3. 理解强制同步布局(强制回流)的概念
  4. 知道如何避免布局抖动
  5. 了解 transform 和 opacity 的性能优势
  6. 能够说出至少 3 种优化回流重绘的方法
  7. 理解 will-change 和 contain 的作用

核心结论

  • 回流必然引起重绘,重绘不一定引起回流
  • 优先使用 transform 和 opacity 做动画
  • 批量读写,避免读写交替
  • 使用类名替代逐个修改样式