MiddleFinger前端面试题

什么是重绘,什么是回流?如何减少回流?

重绘(Repaint)

重绘是指当页面中某些元素的外观(如颜色、背景、边框等)发生改变,但不影响其布局(尺寸和位置)时,浏览器会重新绘制这些元素。

重绘的过程不会影响元素在页面中的布局,因此性能消耗相对较小。

触发重绘的操作

  • 修改颜色(color、background-color)
  • 修改可见性(visibility)
  • 修改阴影(box-shadow)
  • 修改透明度(opacity)
  • 等不改变布局的样式修改

回流(Reflow)

回流(也称为重排)是指当页面中的元素布局(尺寸、位置)发生改变时,浏览器需要重新计算元素的几何属性并重新排列元素。回流比重绘消耗更多的性能资源,因为它涉及到元素位置和尺寸的重新计算。

当发生回流时,通常也会导致重绘,但重绘不一定会引起回流。

触发回流的操作

  • DOM 元素的添加、删除或修改
  • 改变元素的尺寸(width、height、padding、margin、border)
  • 改变元素的位置(position、top、left、right、bottom)
  • 改变元素的内容(文本、图片等)
  • 改变浏览器窗口大小(resize 事件)
  • 计算或读取某些属性(offsetWidth、offsetHeight、clientWidth、clientHeight、scrollWidth、scrollHeight、getComputedStyle()等)
  • 设置 style 属性的值
  • 激活 CSS 伪类(如 :hover)

如何减少回流

由于回流会显著影响性能,所以应尽量减少回流操作。以下是一些减少回流的方法:

1. 批量修改 DOM

避免逐条修改 DOM,而是使用文档片段(DocumentFragment)或者一次性修改。

// 不好的做法
for (let i = 0; i < 100; i++) {
  document.body.appendChild(document.createElement('div'));
}
 
// 好的做法
const fragment = document.createDocumentFragment();
for (let i = 0; i < 100; i++) {
  fragment.appendChild(document.createElement('div'));
}
document.body.appendChild(fragment);

2. 使用 CSS 类一次性修改样式

// 不好的做法
const element = document.getElementById('element');
element.style.width = '100px';
element.style.height = '100px';
element.style.margin = '10px';
 
// 好的做法
const element = document.getElementById('element');
element.classList.add('new-style');
 
// CSS
.new-style {
  width: 100px;
  height: 100px;
  margin: 10px;
}

3. 脱离文档流进行操作

对元素进行复杂操作时,可以先使其脱离文档流,操作完成后再放回去。

// 1. 隐藏元素
const element = document.getElementById('element');
element.style.display = 'none';
// 进行多次修改
element.style.width = '100px';
element.style.height = '100px';
element.style.margin = '10px';
// 显示元素
element.style.display = 'block';
 
// 2. 使用文档片段
const element = document.getElementById('element');
const clone = element.cloneNode(true);
// 对克隆进行修改
clone.style.width = '100px';
// 替换原始元素
element.parentNode.replaceChild(clone, element);
 
// 3. 使用绝对定位
const element = document.getElementById('element');
element.style.position = 'absolute';
// 进行修改
element.style.width = '100px';
// 恢复定位
element.style.position = 'static';

4. 避免频繁读取会引发回流的属性

读取一些布局属性(如 offsetTop、scrollTop 等)会强制浏览器进行回流计算。如果需要多次使用这些值,应该一次性读取并保存。

// 不好的做法
for (let i = 0; i < 100; i++) {
  console.log(element.offsetTop);
}
 
// 好的做法
const offsetTop = element.offsetTop;
for (let i = 0; i < 100; i++) {
  console.log(offsetTop);
}

5. 使用 transform 和 opacity 进行动画

CSS 的 transform 和 opacity 属性通常不会触发回流,只会触发重绘,而且现代浏览器通常会对这些操作进行硬件加速。

/* 不好的做法 */
.box {
  position: absolute;
  left: 0;
  top: 0;
  transition: left 0.5s, top 0.5s;
}
.box:hover {
  left: 100px;
  top: 100px;
}
 
/* 好的做法 */
.box {
  transform: translate(0, 0);
  transition: transform 0.5s;
}
.box:hover {
  transform: translate(100px, 100px);
}

6. 使用 requestAnimationFrame 控制动画帧

function animate() {
  // 执行动画操作
  requestAnimationFrame(animate);
}
requestAnimationFrame(animate);

7. 合理使用节流和防抖

对于频繁触发的事件(如 resize、scroll 等),使用节流(throttle)或防抖(debounce)技术限制事件处理函数的执行频率。

// 防抖函数
function debounce(fn, delay) {
  let timer = null;
  return function() {
    if (timer) clearTimeout(timer);
    timer = setTimeout(() => {
      fn.apply(this, arguments);
    }, delay);
  };
}
 
// 使用防抖处理 resize 事件
window.addEventListener('resize', debounce(function() {
  // 处理 resize 事件
}, 200));

8. 使用 will-change 属性提示浏览器

对于即将发生变化的元素,可以使用 will-change 属性提前告知浏览器,让浏览器进行优化。

.box {
  will-change: transform;
}

注意:will-change 属性不应过度使用,只对确实需要优化的元素使用。

总结

  • 重绘是指元素外观改变但不影响布局的渲染过程
  • 回流是指元素布局(尺寸、位置)改变导致的渲染过程
  • 回流比重绘更消耗性能,回流一定会导致重绘,但重绘不一定会导致回流
  • 减少回流的方法包括:批量修改 DOM、使用 CSS 类、脱离文档流操作、避免频繁读取布局属性、使用 transform 和 opacity 进行动画、使用 requestAnimationFrame、合理使用节流和防抖、使用 will-change 属性等