重排和重绘

47 阅读4分钟

记得上次面试,那个戴着黑框眼镜的面试官扶了扶镜框,幽幽地问我:“说说重排和重绘吧,顺便讲讲怎么优化。”

我心里暗喜,这不撞枪口上了吗?上周刚被这两个概念按在地上摩擦过。

先来个简单粗暴的理解

重排(Reflow)就像是你要重新布置房间家具——床挪到这边,桌子搬到那边,整个房间的格局都变了。这活儿挺累人的,得费不少劲。

重绘(Repaint)就好比你只是把墙刷了个新颜色,或者换了个窗帘。东西还在原地,就是看起来不一样了。这活儿相对轻松点。

所以记住第一个重点:重排必然会引起重绘,但重绘不一定会引起重排。

那些年,我们一起触发的重排

在实际 coding 中,我踩过不少坑。比如这些操作都会触发重排:

// 这些都是重排的“罪魁祸首”
element.style.width = '100px'; // 改尺寸
element.style.margin = '20px'; // 改边距
element.style.display = 'none'; // 显示隐藏
element.innerHTML = '<div>新内容</div>'; // 改内容

// 最坑的是这个——读取某些属性也会强制重排!
const width = element.offsetWidth; // 浏览器:得,先重排一下再告诉你

记得有次我写了个动画,在循环里不停地读取 offsetWidth 然后设置新宽度,页面卡得跟我家十年前的老电脑一样。后来才知道,我在逼着浏览器每秒重排60次,它不卡谁卡?

重绘:相对温和的兄弟

重绘就温和多了,通常只改变视觉样式:

// 这些通常只触发重绘
element.style.color = 'red';
element.style.backgroundColor = '#f0f0f0'; 
element.style.borderRadius = '5px';
element.style.opacity = '0.5';

但别以为重绘就很 cheap,如果频繁触发,照样能让页面掉帧。

我的优化血泪史

1. 批量操作是王道

以前我这么写代码:

// 菜鸟时期的我
const list = document.getElementById('myList');
for (let i = 0; i < 100; i++) {
    const item = document.createElement('li');
    item.textContent = `Item ${i}`;
    list.appendChild(item); // 每次append都重排,重排100次!
}

页面卡成狗之后,我学乖了:

// 现在的我
const list = document.getElementById('myList');
const fragment = document.createDocumentFragment();

for (let i = 0; i < 100; i++) {
    const item = document.createElement('li');
    item.textContent = `Item ${i}`;
    fragment.appendChild(item);
}

list.appendChild(fragment); // 只重排1次!

2. 读写要分开

这是我被同事 code review 时指出的问题:

// 错误示范 - 读写混合
const width = element.offsetWidth; // 读(重排)
element.style.width = width + 10 + 'px'; // 写
const height = element.offsetHeight; // 读(再次重排)
element.style.height = height + 10 + 'px'; // 写

// 正确姿势 - 先读后写
const width = element.offsetWidth; // 读
const height = element.offsetHeight; // 读
element.style.width = width + 10 + 'px'; // 写
element.style.height = height + 10 + 'px'; // 写

3. CSS3 动画拯救世界

以前我用 jQuery 动画:

$('.element').animate({ left: '100px' }, 500); // 触发重排

现在我用 transform:

.element {
    transition: transform 0.5s;
}
.element:hover {
    transform: translateX(100px); /* 只触发重绘,性能起飞! */
}

为什么 transform 这么牛?因为浏览器会为它创建单独的合成层,用 GPU 加速,完全跳过重排和重绘。

4. 现代框架的魔法

React、Vue 这些框架为什么快?因为它们用了虚拟 DOM:

function MyComponent() {
    const [items, setItems] = useState([]);
    
    // 即使我一次添加10个item,React也会批量处理
    const addItems = () => {
        setItems(prev => [...prev, ...newItems]);
    };
    
    return (
        <div>
            {items.map(item => <div key={item.id}>{item.name}</div>)}
        </div>
    );
}

框架在背后帮我们做了很多优化,但理解原理还是很重要的,不然你照样能写出卡顿的代码。

调试技巧:看看谁在搞事情

Chrome DevTools 是我的好朋友:

  1. Performance 面板:录制一下就能看到哪些操作触发了重排重绘
  2. Rendering 面板:开启 "Paint flashing",重绘的区域会绿色闪烁
  3. Layers 面板:看看哪些元素被提升到了合成层

有次我用 Paint flashing 发现一个小按钮的 hover 效果导致整个页面重绘,原来是用了 box-shadow 动画。改成 transform 后性能立马提升。

面试时的装逼技巧

当面试官问这个问题时,我是这么回答的:

"重排可以理解为浏览器重新计算布局,代价比较大;重绘是重新绘制外观,代价相对小点。在实际项目中,我会通过批量DOM操作、使用CSS3动画、避免频繁读写布局属性来优化性能。比如上周我刚优化了一个列表渲染性能问题,用 DocumentFragment 把重排次数从100次降到了1次,滚动流畅了很多。"

最后说两句

重排和重绘就像前端开发的内功心法,理解它们不一定能让你立刻写出炫酷的效果,但能保证你写的东西不卡顿。

现在我再看到那些说“前端就是切图写页面”的人,都会微微一笑。这年头,写个不卡顿的页面可比写炫酷效果难多了。


本文来自一个被重排重绘折磨过的前端开发者,希望对你有帮助。如果文章有哪里说错了,那一定是我故意的,为了测试你有没有认真看。