理解DOM性能優(yōu)化:從重繪回流到虛擬 DOM
一、什么是重繪與回流?
想象一下你在畫畫:
- 重繪(Repaint):就像只改變畫作的某些顏色,但整體布局不變。
- 回流(Reflow):相當于把整幅畫重新構(gòu)圖,所有元素的位置大小都要重新計算。
在瀏覽器中:
- 回流:當 DOM 的變化影響了元素的幾何屬性(如寬度、高度、位置等), 瀏覽器需要重新計算元素的幾何屬性,并重新構(gòu)建渲染樹。
- 重繪:當元素的外觀屬性(如顏色、背景色等)發(fā)生改變,但不影響布局時,瀏覽器只需重繪元素。
回流必定會引起重繪,但重繪不一定會引起回流。
導致回流的常見操作:
//幾何屬性變化
element.style.width = '100px';
element.style.height = '100px';
element.style.padding = '10px';
//添加或刪除可見DOM元素
document.body.appendChild(newElement);
element.parentNode.removeChild(element);
//內(nèi)容變化(如文本改變導致尺寸變化)
element.innerHTML = '新內(nèi)容';
//瀏覽器窗口大小改變
window.addEventListener('resize', callback);
//獲取某些屬性(會強制觸發(fā)回流以獲取最新值)
const width = element.offsetWidth;
const height = element.offsetHeight;只導致重繪的操作:
//顏色相關
element.style.color = 'red';
element.style.backgroundColor = '#fff';
//邊框樣式
element.style.border = '1px solid red';
//可見性
element.style.visibility = 'hidden';瀏覽器渲染流程是這樣的:
- 解析HTML生成DOM樹.
- 解析CSS生成CSSOM樹.
- 將DOM和CSSOM合并成渲染樹(Render Tree).
- 計算渲染樹的布局(回流).
- 將布局繪制到屏幕上(重繪).
每次回流都需要重新計算所有受影響元素的幾何屬性, 然后重繪. 這在復雜頁面中會非常消耗性能.
舉個例子:
const box = document.getElementById('box');
for (let i = 0; i < 100; i++) {
box.style.width = box.offsetWidth + 1 + 'px'; //每次循環(huán)都讀取和設置寬度,強制回流
}這段代碼會導致 100 次回流, 性能非常差。
二、最小化DOM操作
2.1 批量修改DOM
不要一個一個地修改DOM, 而是先把所有修改準備好, 然后一次性應用。
不好的做法:
const list = document.getElementById('list');
for (let i = 0; i < 10; i++) {
const item = document.createElement('li');
item.textContent = `Item ${i}`;
list.appendChild(item); //每次循環(huán)都操作DOM
}好的做法:
const list = document.getElementById('list');
const fragment = document.createDocumentFragment();//使用文檔片段
for (let i = 0; i < 10; i++) {
const item = document.createElement('li');
item.textContent = `Item ${i}`;
fragment.appendChild(item); //先在內(nèi)存中操作
}
list.appendChild(fragment); //最后一次性插入DOM2.2 使用className而不是style
直接操作 style 屬性會導致多次重繪/回流, 而修改 className 可以批量應用樣式。
不好的做法:
element.style.color = 'red';
element.style.backgroundColor = 'blue';
element.style.fontSize = '16px';好的做法:
.highlight {
color: red;
background-color: blue;
font-size: 16px;
}element.classList.add('highlight');2.3 脫離文檔流后再進行復雜操作
如果你需要對一個元素進行多次復雜的 DOM 操作, 可以先將它從文檔流中移除, 操作完成后再添加回去。
const list = document.getElementById('list');
const parent = list.parentNode;
//從DOM中移除元素
parent.removeChild(list);
//進行復雜的DOM操作
//...比如添加很多子元素
//操作完成后再添加回DOM
parent.appendChild(list);2.4 避免頻繁讀取會觸發(fā)回流的屬性
像offsetWidth、offsetHeight、getComputedStyle等屬性會強制瀏覽器觸發(fā)回流來獲取最新值。
不好的做法:
for (let i = 0; i < boxes.length; i++) {
const width = boxes[i].offsetWidth; //每次循環(huán)都讀取,強制回流
boxes[i].style.width = width + 10 + 'px';
}好的做法:
//先讀取所有值
const widths = [];
for (let i = 0; i < boxes.length; i++) {
widths[i] = boxes[i].offsetWidth;
}
//然后統(tǒng)一設置
for (let i = 0; i < boxes.length; i++) {
boxes[i].style.width = widths[i] + 10 + 'px';
}2.5 使用CSS3動畫替代JS動畫
CSS3 動畫可以利用硬件加速, 通常比 JavaScript 實現(xiàn)的動畫性能更好。
不好的做法:
//使用JavaScript實現(xiàn)動畫
function animate(element) {
let pos = 0;
const id = setInterval(() => {
if (pos >= 100) clearInterval(id);
else {
pos++;
element.style.left = pos + 'px';
}
}, 10);
}好的做法:
.box {
transition: left 1s ease-out;
}三、虛擬DOM概念介紹
3.1 什么是虛擬DOM?
虛擬DOM(Virtual DOM)是一個用JavaScript對象表示的DOM樹的副本. React、Vue等現(xiàn)代前端框架都使用了虛擬DOM的概念。
//虛擬DOM對象的簡化表示
const virtualNode = {
tag: 'div',
props: { id: 'app', class: 'container' },
children: [
{
tag: 'h1',
props: {},
children: ['Hello, Virtual DOM!']
}
]
};3.2 虛擬DOM如何工作?
- 初始渲染: 根據(jù)組件創(chuàng)建虛擬DOM樹
- 狀態(tài)變化: 當數(shù)據(jù)變化時,創(chuàng)建新的虛擬DOM樹
- 差異比較(Diffing): 比較新舊虛擬DOM樹的差異
- 批量更新: 只將差異部分應用到真實DOM
3.3 為什么虛擬DOM能提高性能?
- 減少直接 DOM 操作: 批量更新, 減少回流和重繪。
- 高效的差異算法: 只更新必要的部分。
- 跨平臺能力: 虛擬DOM可以渲染到不同平臺(Web、Native等)。
3.4 讓我們實現(xiàn)一個超簡版的虛擬DOM來理解其原理
//創(chuàng)建虛擬DOM節(jié)點的函數(shù)
function h(tag, props, children) {
return { tag, props, children };
}
//將虛擬DOM渲染為真實DOM
function render(vnode) {
if (typeof vnode === 'string') {
return document.createTextNode(vnode);
}
const el = document.createElement(vnode.tag);
//設置屬性
for (const [key, value] of Object.entries(vnode.props || {})) {
el.setAttribute(key, value);
}
//渲染子節(jié)點
(vnode.children || []).forEach(child => {
el.appendChild(render(child));
});
return el;
}
//使用示例
const vdom = h('div', { id: 'app' }, [
h('h1', {}, ['Hello Virtual DOM']),
h('p', {}, ['This is a simple example'])
]);
const realDOM = render(vdom);
document.body.appendChild(realDOM);
圖片




























