📌 面试官视角
在前端面试中,"如何减少页面重绘跟重排"是一道高频且经典的面试题。这道题不仅考察你对浏览器渲染机制的理解,还能看出你的性能优化意识和实际项目经验。掌握这个知识点,能让你在面试中脱颖而出。
一、面试题分析
❓ 面试题:如何减少页面重绘跟重排?
这道面试题看似简单,实则考察了多个层面的知识:
- 基础概念:什么是重绘?什么是重排?它们有什么区别?
- 触发条件:哪些操作会触发重绘?哪些操作会触发重排?
- 优化策略:如何减少重绘和重排?有哪些具体的优化方法?
- 实践经验:在实际项目中如何应用这些优化策略?
💡 面试加分点
能够从浏览器渲染原理出发,结合具体代码示例,说明优化策略的实际应用,并能分享项目中的实践经验。
二、重绘和重排的概念
1. 什么是重排(Reflow)?
重排,也叫回流,是指浏览器需要重新计算页面元素的几何属性(位置、大小、边距等)的过程。
DOM改变 → 重新计算布局 → 生成渲染树 → 重排
2. 什么是重绘(Repaint)?
重绘是指浏览器需要重新绘制页面元素的外观(颜色、背景、边框等)的过程。
样式改变 → 重新绘制 → 重绘
3. 重排和重绘的关系
重要结论:重排一定会导致重绘,但重绘不一定导致重排。
| 特性 | 重排(Reflow) | 重绘(Repaint) |
|---|---|---|
| 触发条件 | 元素几何属性改变 | 元素外观属性改变 |
| 性能消耗 | 大(需要重新计算布局) | 小(只需重新绘制) |
| 是否触发对方 | 会触发重绘 | 不一定触发重排 |
| 常见操作 | 改变宽高、位置、边距等 | 改变颜色、背景、边框等 |
三、触发重排和重绘的操作
1. 触发重排的操作
📝 触发重排的常见操作
javascript
// 1. 改变元素的几何属性
element.style.width = '200px';
element.style.height = '200px';
element.style.margin = '10px';
element.style.padding = '10px';
element.style.border = '1px solid #000';
// 2. 改变元素的位置
element.style.top = '100px';
element.style.left = '100px';
element.style.position = 'absolute';
// 3. 操作DOM树
document.body.appendChild(element);
element.parentNode.removeChild(element);
// 4. 改变字体大小
element.style.fontSize = '20px';
// 5. 获取布局信息(强制同步布局)
const width = element.offsetWidth;
const height = element.offsetHeight;
const top = element.offsetTop;
2. 触发重绘的操作
📝 触发重绘的常见操作
javascript
// 1. 改变颜色
element.style.color = 'red';
element.style.backgroundColor = 'blue';
element.style.borderColor = 'green';
// 2. 改变背景
element.style.background = 'url(image.jpg)';
element.style.backgroundImage = 'url(image.jpg)';
// 3. 改变透明度
element.style.opacity = '0.5';
// 4. 改变可见性
element.style.visibility = 'hidden';
// 5. 改变文本样式
element.style.textDecoration = 'underline';
element.style.textShadow = '2px 2px 2px #000';
⚠️ 强制同步布局
在JavaScript中,某些操作会强制浏览器立即执行布局计算,这被称为"强制同步布局"(Forced Synchronous Layout)。这种操作会导致性能问题,应该尽量避免。
四、减少重排和重绘的策略
1. 使用 transform 和 opacity 代替 top/left 和 visibility
❓ 面试题:为什么 transform 和 opacity 性能更好?
transform 和 opacity 不会触发重排,只会触发重绘,而且可以利用 GPU 加速。
📝 性能对比
javascript
// ❌ 不推荐:会触发重排
element.style.left = '100px';
element.style.top = '100px';
element.style.visibility = 'hidden';
// ✅ 推荐:只触发重绘,GPU加速
element.style.transform = 'translate(100px, 100px)';
element.style.opacity = '0';
💡 原理说明
transform 和 opacity 不会改变元素的布局,只是改变元素的绘制方式。浏览器可以将这些操作交给 GPU 处理,从而提高性能。
2. 批量操作 DOM
❓ 面试题:如何批量操作 DOM 以减少重排?
将多次 DOM 操作合并为一次,可以大大减少重排次数。
📝 批量操作 DOM
javascript
// ❌ 不推荐:每次操作都会触发重排
for (let i = 0; i < 100; i++) {
const li = document.createElement('li');
li.textContent = 'Item ' + i;
document.getElementById('list').appendChild(li);
}
// ✅ 推荐:使用文档片段批量操作
const fragment = document.createDocumentFragment();
for (let i = 0; i < 100; i++) {
const li = document.createElement('li');
li.textContent = 'Item ' + i;
fragment.appendChild(li);
}
document.getElementById('list').appendChild(fragment);
3. 避免强制同步布局
❓ 面试题:什么是强制同步布局?如何避免?
强制同步布局是指在 JavaScript 中读取布局信息后立即修改布局,这会强制浏览器立即执行布局计算。
📝 避免强制同步布局
javascript
// ❌ 不推荐:强制同步布局
function resizeAll() {
const elements = document.querySelectorAll('.item');
elements.forEach(element => {
const width = element.offsetWidth; // 读取布局
element.style.width = width + 10 + 'px'; // 立即修改布局
});
}
// ✅ 推荐:分离读取和修改操作
function resizeAll() {
const elements = document.querySelectorAll('.item');
const widths = [];
elements.forEach(element => {
widths.push(element.offsetWidth); // 先读取所有布局
});
elements.forEach((element, index) => {
element.style.width = widths[index] + 10 + 'px'; // 再修改所有布局
});
}
4. 使用虚拟 DOM
❓ 面试题:虚拟 DOM 如何减少重排和重绘?
虚拟 DOM 通过 Diff 算法计算出最小的变更,然后批量更新真实 DOM,从而减少重排和重绘的次数。
📝 虚拟 DOM 的工作原理
javascript
// 虚拟 DOM 的核心思想
// 1. 创建虚拟 DOM 树
const virtualDOM = {
tag: 'div',
props: { className: 'container' },
children: [
{ tag: 'h1', props: {}, children: ['Hello'] },
{ tag: 'p', props: {}, children: ['World'] }
]
};
// 2. Diff 算法比较新旧虚拟 DOM
// 3. 计算出最小的变更
// 4. 批量更新真实 DOM
💡 原理说明
虚拟 DOM 的核心思想是:在内存中维护一个虚拟的 DOM 树,当数据变化时,先比较新旧虚拟 DOM 树的差异,然后只将差异部分应用到真实 DOM 上,从而减少重排和重绘的次数。
5. 使用 CSS 动画
❓ 面试题:CSS 动画如何减少重排和重绘?
CSS 动画可以利用 GPU 加速,不会触发重排,只会触发重绘。
📝 CSS 动画示例
css
/* ❌ 不推荐:使用 JavaScript 动画,会触发重排 */
.element {
position: absolute;
left: 0;
top: 0;
}
/* ✅ 推荐:使用 CSS 动画,GPU加速 */
.element {
transform: translateX(0);
transition: transform 0.3s ease;
}
.element:hover {
transform: translateX(100px);
}
/* ✅ 推荐:使用 CSS 动画,GPU加速 */
@keyframes slideIn {
from {
transform: translateX(-100%);
}
to {
transform: translateX(0);
}
}
.element {
animation: slideIn 0.3s ease;
}
💡 原理说明
CSS 动画(特别是使用 transform 和 opacity 的动画)可以利用 GPU 加速,不会触发重排,只会触发重绘,从而提高性能。
6. 使用 will-change 属性
❓ 面试题:will-change 属性如何优化性能?
will-change 属性可以提前告知浏览器哪些属性会发生变化,让浏览器提前做好优化准备。
📝 will-change 属性示例
css
/* ❌ 不推荐:滥用 will-change */
* {
will-change: transform, opacity;
}
/* ✅ 推荐:只在需要时使用 */
.element {
will-change: transform;
}
.element:hover {
transform: translateX(100px);
}
/* ✅ 推荐:动画结束后移除 will-change */
.element {
will-change: transform;
animation: slideIn 0.3s ease;
}
.element.animation-end {
will-change: auto;
}
💡 原理说明
will-change 属性可以提前告知浏览器哪些属性会发生变化,让浏览器提前做好优化准备(如创建新的图层)。但是,滥用 will-change 会导致性能问题,应该只在需要时使用。
7. 使用 requestAnimationFrame
❓ 面试题:requestAnimationFrame 如何优化性能?
requestAnimationFrame 可以根据浏览器的刷新率来执行动画,避免不必要的重绘。
📝 requestAnimationFrame 示例
javascript
// ❌ 不推荐:使用 setInterval,可能导致不必要的重绘
function animate() {
element.style.left = parseInt(element.style.left) + 1 + 'px';
setInterval(animate, 16);
}
// ✅ 推荐:使用 requestAnimationFrame,根据浏览器刷新率执行
function animate() {
element.style.transform = `translateX(${parseInt(element.style.transform.match(/\d+/)[0]) + 1}px)`;
requestAnimationFrame(animate);
}
requestAnimationFrame(animate);
💡 原理说明
requestAnimationFrame 可以根据浏览器的刷新率(通常是 60Hz)来执行动画,避免不必要的重绘。当页面不可见时,requestAnimationFrame 会自动暂停,从而节省性能。
8. 使用离屏渲染
❓ 面试题:什么是离屏渲染?如何使用?
离屏渲染是指在内存中进行复杂的绘制操作,然后将结果绘制到页面上,从而减少重排和重绘的次数。
📝 离屏渲染示例
javascript
// ❌ 不推荐:直接在页面上绘制,会触发多次重绘
function drawComplexShape() {
const canvas = document.getElementById('canvas');
const ctx = canvas.getContext('2d');
ctx.fillStyle = 'red';
ctx.fillRect(0, 0, 100, 100);
ctx.fillStyle = 'blue';
ctx.fillRect(100, 0, 100, 100);
ctx.fillStyle = 'green';
ctx.fillRect(0, 100, 100, 100);
}
// ✅ 推荐:在内存中绘制,然后一次性绘制到页面上
function drawComplexShape() {
const canvas = document.getElementById('canvas');
const ctx = canvas.getContext('2d');
const offscreenCanvas = document.createElement('canvas');
const offscreenCtx = offscreenCanvas.getContext('2d');
offscreenCtx.fillStyle = 'red';
offscreenCtx.fillRect(0, 0, 100, 100);
offscreenCtx.fillStyle = 'blue';
offscreenCtx.fillRect(100, 0, 100, 100);
offscreenCtx.fillStyle = 'green';
offscreenCtx.fillRect(0, 100, 100, 100);
ctx.drawImage(offscreenCanvas, 0, 0);
}
💡 原理说明
离屏渲染是指在内存中进行复杂的绘制操作,然后将结果绘制到页面上,从而减少重排和重绘的次数。这种方法特别适合复杂的动画和图形绘制。
五、实践经验总结
1. 给面试者的建议
🎯 给面试者的建议
- 理解原理:深入理解浏览器渲染机制,知道重排和重绘的区别
- 掌握策略:掌握减少重排和重绘的各种策略
- 实践应用:在实际项目中应用这些优化策略
- 性能测试:使用 Chrome DevTools 进行性能测试和优化
- 持续优化:持续关注性能优化,不断改进代码
- 分享经验:和团队分享性能优化的经验和技巧
- 关注新特性:关注浏览器的新特性和优化方案
2. 面试中的回答策略
🎯 面试中的回答策略
- 先说概念:先解释什么是重排和重绘,以及它们的区别
- 再说触发条件:说明哪些操作会触发重排和重绘
- 然后说优化策略:详细说明减少重排和重绘的各种策略
- 最后说实践经验:分享在实际项目中如何应用这些优化策略
- 结合代码示例:用具体的代码示例说明优化策略的应用
3. 避免的误区
⚠️ 避免的误区
- 过度优化:不要过度优化,要根据实际情况选择合适的优化策略
- 滥用 will-change:不要滥用 will-change 属性,会导致性能问题
- 忽视用户体验:不要为了性能而牺牲用户体验
- 不测试性能:不要凭感觉优化,要用工具测试性能
- 不关注兼容性:不要忽视浏览器兼容性问题
4. 项目中的实际应用
📝 项目中的实际应用
- 列表渲染优化:使用虚拟 DOM 或文档片段批量操作 DOM
- 动画优化:使用 CSS 动画或 requestAnimationFrame
- 图片懒加载:使用 IntersectionObserver API 实现图片懒加载
- 虚拟滚动:对于长列表,使用虚拟滚动技术
- 代码分割:使用 Webpack 的代码分割功能,按需加载
💡 总结
减少页面重绘和重排是前端性能优化的重要手段。通过深入理解浏览器渲染机制,掌握各种优化策略,并在实际项目中应用这些策略,可以大大提高页面的性能。在面试中,能够从原理出发,结合具体代码示例,说明优化策略的实际应用,并能分享项目中的实践经验,会让你在面试中脱颖而出。
will-change 跟 conian 可以参考我之前的文章:
优化重排跟重绘,怎么少的了 will-change
使用CSS Contain 优化你的页面(重排和重绘)
感谢阅读!如果您有任何问题或建议,欢迎在评论区留言讨论。
如果你觉得本文对你有帮助,欢迎点赞、收藏、分享,也欢迎关注我,获取更多前端技术干货!