DOM 操作优化与重排重绘
理解浏览器渲染机制是优化DOM操作的关键,减少重排重绘能显著提升页面性能。
渲染流水线
渲染阶段
JavaScript
JavaScript → Style → Layout → Paint → Composite
(样式计算) (布局) (绘制) (合成)
关键概念
- 重排(Reflow/Layout):重新计算元素几何属性
- 重绘(Repaint/Paint):重新绘制元素视觉样式
- 合成(Composite):将各层合成最终画面
性能影响
JavaScript
重排:触发完整流水线 → Style + Layout + Paint + Composite
重绘:触发部分流水线 → Style + Paint + Composite
合成:只触发合成 → Composite(最快)
重排触发条件
常见触发操作
JavaScript
// 改变几何属性触发重排
element.style.width = '100px'; // ✗ 重排
element.style.height = '50px'; // ✗ 重排
element.style.top = '10px'; // ✗ 重排
element.style.left = '20px'; // ✗ 重排
element.style.margin = '10px'; // ✗ 重排
element.style.padding = '5px'; // ✗ 重排
element.style.display = 'block'; // ✗ 重排
// 改变字体触发重排
element.style.fontSize = '16px'; // ✗ 重排
element.style.fontWeight = 'bold'; // ✗ 重排
// 读取几何属性触发重排
const width = element.offsetWidth; // ✗ 强制同步重排
const height = element.offsetHeight; // ✗ 强制同步重排
const top = element.offsetTop; // ✗ 强制同步重排
const left = element.offsetLeft; // ✗ 强制同步重排
// DOM操作触发重排
document.body.appendChild(element); // ✗ 重排
element.removeChild(child); // ✗ 重排
重绘触发条件
常见触发操作
JavaScript
// 改变视觉样式触发重绘(不改变几何)
element.style.color = 'red'; // ✓ 重绘(不重排)
element.style.backgroundColor = '#fff'; // ✓ 重绘
element.style.visibility = 'hidden'; // ✓ 重绘(opacity:0重绘,visibility:hidden重排)
element.style.border = '1px solid red'; // ✓ 重绘(不改变尺寸)
element.style.boxShadow = '0 0 10px rgba(0,0,0,0.5)'; // ✓ 重绘
优化策略
批量DOM修改
JavaScript
// ❌ 多次单独修改触发多次重排
element.style.width = '100px';
element.style.height = '50px';
element.style.margin = '10px';
// ✅ 使用class一次性修改
element.className = 'updated'; // 只触发一次重排
// CSS
.updated {
width: 100px;
height: 50px;
margin: 10px;
}
// ✅ 使用cssText
element.style.cssText = 'width:100px;height:50px;margin:10px;';
// ✅ 批量样式对象
Object.assign(element.style, {
width: '100px',
height: '50px',
margin: '10px'
});
离线DOM操作
JavaScript
// ❌ 直接操作DOM触发多次重排
const list = document.getElementById('list');
for (let i = 0; i < 100; i++) {
const item = document.createElement('li');
list.appendChild(item); // 每次append触发重排
}
// ✅ 使用DocumentFragment
const fragment = document.createDocumentFragment();
for (let i = 0; i < 100; i++) {
const item = document.createElement('li');
fragment.appendChild(item);
}
list.appendChild(fragment); // 只触发一次重排
// ✅ 克隆节点离线操作
const clone = element.cloneNode(true);
clone.style.width = '100px';
clone.style.height = '50px';
element.parentNode.replaceChild(clone, element);
批量读写分离
JavaScript
// ❌ 读写交替触发强制同步重排
for (let i = 0; i < 100; i++) {
const width = elements[i].offsetWidth; // 强制重排
elements[i].style.width = width + 10 + 'px'; // 又触发重排
}
// ✅ 先读后写,分离操作
const widths = elements.map(el => el.offsetWidth); // 批量读取
elements.forEach((el, i) => {
el.style.width = widths[i] + 10 + 'px'; // 批量写入
});
使用Flexbox/Grid
JavaScript
// ❌ 使用float/layout触发更多重排
.container {
float: left;
}
// ✅ 使用Flexbox/Grid,重排影响更小
.container {
display: flex;
flex-direction: row;
}
提升合成层
will-change提示
JavaScript
// 提示浏览器提前优化
.moving-element {
will-change: transform, opacity;
}
// 动态添加will-change
element.addEventListener('mouseenter', () => {
element.style.willChange = 'transform';
});
element.addEventListener('animationend', () => {
element.style.willChange = 'auto'; // 动画结束移除
});
transform替代位置属性
JavaScript
// ❌ 使用top/left触发重排
.element {
position: absolute;
top: 100px;
left: 50px;
}
// ✅ 使用transform触发合成
.element {
transform: translate(50px, 100px);
}
// 动画使用transform
.element {
animation: move 1s;
}
@keyframes move {
to { transform: translateX(100px); }
}
// ❌ 使用width/height动画
@keyframes resize {
to { width: 200px; } /* 每帧重排 */
}
// ✅ 使用transform scale
@keyframes resize {
to { transform: scaleX(2); } /* 只触发合成 */
}
opacity替代visibility
JavaScript
// ❌ visibility:hidden触发重排
.hidden-element {
visibility: hidden;
}
// ✅ opacity:0只触发重绘/合成
.hidden-element {
opacity: 0;
}
强制同步重排问题
常见陷阱
JavaScript
// 强制同步重排:修改后立即读取几何属性
element.style.width = '100px';
const height = element.offsetHeight; // 强制立即重排
// 循环中触发多次重排
for (let i = 0; i < elements.length; i++) {
elements[i].style.width = '100px';
console.log(elements[i].offsetWidth); // 每次强制重排
}
避免强制重排
JavaScript
// 使用requestAnimationFrame
function updateLayout() {
element.style.width = '100px';
requestAnimationFrame(() => {
const height = element.offsetHeight; // 在下一帧读取
});
}
// 使用FastDOM模式
const fastdom = {
reads: [],
writes: [],
read(fn) {
this.reads.push(fn);
this.schedule();
},
write(fn) {
this.writes.push(fn);
this.schedule();
},
schedule() {
if (!this.scheduled) {
this.scheduled = true;
requestAnimationFrame(() => this.flush());
}
},
flush() {
this.reads.forEach(fn => fn());
this.reads = [];
this.writes.forEach(fn => fn());
this.writes = [];
this.scheduled = false;
}
};
// 使用
fastdom.read(() => {
const width = element.offsetWidth;
});
fastdom.write(() => {
element.style.width = width + 10 + 'px';
});
性能检测工具
Chrome DevTools
text
// Performance面板
// 1. 打开DevTools → Performance
// 2. 录制页面操作
// 3. 查看Layout/Paint事件
// Rendering面板
// 1. DevTools → More tools → Rendering
// 2. 勾选"Paint flashing"(重绘区域闪烁)
// 3. 勾选"Layout shift regions"(布局抖动区域)
performance API
text
// 监听长任务
const observer = new PerformanceObserver((list) => {
for (const entry of list.getEntries()) {
console.log('Long task:', entry);
}
});
observer.observe({ entryTypes: ['longtask'] });
// 测量渲染性能
const start = performance.now();
element.style.width = '100px';
const end = performance.now();
console.log('DOM操作耗时:', end - start);
要点总结
- 渲染流水线:Style → Layout → Paint → Composite
- 重排代价最高:触发完整流水线,改变几何属性触发
- 批量操作:class切换、DocumentFragment、读写分离
- 提升合成层:will-change、transform、opacity
- 避免强制重排:读写交替问题,使用requestAnimationFrame
- 检测工具:DevTools Performance、Paint flashing
存放路径:articles/JS/专家/高级性能分析/DOM 操作优化与重排重绘.md
📝 发现内容有误?点击此处直接编辑