怎么理解回流跟重绘?什么场景下会触发?
问题解析
回流(Reflow)和重绘(Repaint)是浏览器渲染机制的核心概念,直接影响页面性能。面试考察这个问题,是想要了解候选人对浏览器工作原理的理解,以及能否写出高性能的 CSS。
核心概念
渲染流程
HTML → DOM Tree
↓
CSS → CSSOM Tree
↓
Render Tree(渲染树)
↓
Layout(布局/回流)→ 计算几何信息
↓
Paint(绘制/重绘)→ 绘制像素
↓
Composite(合成)→ 显示到屏幕
什么是回流(Reflow)
回流(也叫重排)是当渲染树中的元素尺寸、结构或位置发生变化时,浏览器重新计算元素几何属性的过程。
触发回流后:
- 受影响的部分需要重新布局
- 通常会导致后续元素的位置变化
- 一定会触发重绘
什么是重绘(Repaint)
重绘是当元素的外观(颜色、背景等)发生变化但不影响布局时,浏览器重新绘制元素的过程。
触发重绘后:
- 仅重新绘制视觉表现
- 布局不会改变
- 性能开销小于回流
合成(Composite)
现代浏览器使用 GPU 加速的合成阶段:
- 将页面分层,独立绘制
- 通过 GPU 合成最终图像
transform和opacity的修改只触发合成
触发场景
触发回流的操作
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: transformopacity动画position: fixedvideo,canvas,iframe
面试要点
- 能够清晰解释回流和重绘的区别
- 能够列举触发回流和重绘的常见属性
- 理解强制同步布局(强制回流)的概念
- 知道如何避免布局抖动
- 了解 transform 和 opacity 的性能优势
- 能够说出至少 3 种优化回流重绘的方法
- 理解 will-change 和 contain 的作用
核心结论:
- 回流必然引起重绘,重绘不一定引起回流
- 优先使用 transform 和 opacity 做动画
- 批量读写,避免读写交替
- 使用类名替代逐个修改样式