记得上次面试,那个戴着黑框眼镜的面试官扶了扶镜框,幽幽地问我:“说说重排和重绘吧,顺便讲讲怎么优化。”
我心里暗喜,这不撞枪口上了吗?上周刚被这两个概念按在地上摩擦过。
先来个简单粗暴的理解
重排(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 是我的好朋友:
- Performance 面板:录制一下就能看到哪些操作触发了重排重绘
- Rendering 面板:开启 "Paint flashing",重绘的区域会绿色闪烁
- Layers 面板:看看哪些元素被提升到了合成层
有次我用 Paint flashing 发现一个小按钮的 hover 效果导致整个页面重绘,原来是用了 box-shadow 动画。改成 transform 后性能立马提升。
面试时的装逼技巧
当面试官问这个问题时,我是这么回答的:
"重排可以理解为浏览器重新计算布局,代价比较大;重绘是重新绘制外观,代价相对小点。在实际项目中,我会通过批量DOM操作、使用CSS3动画、避免频繁读写布局属性来优化性能。比如上周我刚优化了一个列表渲染性能问题,用 DocumentFragment 把重排次数从100次降到了1次,滚动流畅了很多。"
最后说两句
重排和重绘就像前端开发的内功心法,理解它们不一定能让你立刻写出炫酷的效果,但能保证你写的东西不卡顿。
现在我再看到那些说“前端就是切图写页面”的人,都会微微一笑。这年头,写个不卡顿的页面可比写炫酷效果难多了。
本文来自一个被重排重绘折磨过的前端开发者,希望对你有帮助。如果文章有哪里说错了,那一定是我故意的,为了测试你有没有认真看。