徹底澄清“Virtual DOM 飛快”的神話。
注意:原文發表於2018-12-27,隨着框架不斷演進,部分內容可能已不適用。
近年來,如果你有使用過 JavaScript 框架,那么你可能聽說過“Virtual DOM 飛快”,甚至認為比真實的 DOM 還要快。
令人震驚的是,這種說法竟然深入人心。
有人曾問我 Svelte 不使用 Virtual DOM,它為何更快?看來現在是時候仔細探討一下。
什么是 Virtual DOM?
在眾多框架中,你通常是使用 render()
函數來構建應用程序UI的,就像下方這個簡單的 React
組件:
function HelloMessage(props) {
return (
<div className="greeting">
Hello {props.name}
</div>
);
}
不用 JSX,你一樣可以做同樣的事情……
function HelloMessage(props) {
return React.createElement(
'div',
{ className: 'greeting' },
'Hello ',
props.name
);
}
……后者正是前者的宗本道源,其結果自然一致:代表的是頁面渲染的對象。
這個對象就是 Virtual DOM。
一旦程序更新了狀態(例如 name
屬性被修改),便會創建新的對象。
框架要做的工作是對比新舊對象之間的差異,找出需要進行重新渲染的部分,並將其應用到真實的 DOM 中。
這種觀念是如何開始的?
關於 Virtual DOM 性能的誤解,可以追溯到 React 正式發布那會。
在2013年,React 前團隊核心成員 Pete Hunt 在《重新思考最佳實踐》的演講中提到:
這確實是快如閃電,主要是因為大多數 DOM 操作慢如蝸牛, DOM 有很多性能上的開銷,大多數 DOM 操作往往會掉鏈子。
截圖來自 JSConf EU 2013 《重新思考最佳實踐》
但是 —— 慢着!
Virtual DOM 只是真實 DOM 操作錦上添花的補充而已。
它之所以快,是因為拿性能更差的框架做對比(在2013年,可以欺負的選擇有很多!),另一種選擇是做一些他人不屑去做的事情:
onEveryStateChange(() => {
document.body.innerHTML = renderMyApp();
});
Pete 很快就澄清……
React 不是魔法。
就像你可以使用C進入匯編程序並擊潰C編譯器一樣,你可以進入原生 DOM 操作和 DOM API 調用,並在時機來臨時擊潰 React。
然而,使用 C、Java 或者 JavaScript,可以將性能提升一個數量級,你不必擔心……平台的細節。
使用 React,你可以構建應用程序時無需顧及性能問題,它本身就很快。
…… 但這還是沒有撓到癢處。
那么……Virtual DOM 慢嗎?
並不盡然。
如果能夠防患於未然,那確實“Virtual DOM 飛快”。
React 最初的承諾是,你可以在每次狀態改變時,自動重新渲染你的整個應用,且不用擔心性能。
不敢苟同。
果真如此,那就不需要像 shouldComponentUpdate
這樣的優化了(這是一個用於告訴 React 何時可以安全地跳過一個組件的方法)。
就算用了 shouldComponentUpdate
,一次性更新整個應用的 Virtual DOM 也大費周折。
前不久 React 團隊引入一種叫 React Fiber 的東西,它可以將更新划分成較小的塊。
這意味着(除了其他事項外)更新不會長時間阻塞主線程,盡管它不會減少工作總量或總體耗時。
開銷從何而來?
顯而易見,DOM差異比較(diffing)並非毫無代價。
這必須先將新的 Virtual DOM 與舊的差異(快照)進行比較,然后才能對真實 DOM 應用更改。
就拿前面的 HelloMessage
為例,假設 name
屬性從“world”更改為“everybody”:
-
兩個快照都包含一個元素,在這種情況下,它都是
<div>
,這意味着我們可以保持相同的 DOM 節點。 -
我們枚舉
<div>
舊的和新的所有屬性,以查看是否需要更改、添加或者刪除任何屬性。在這兩種情況下,我們都有一個特性,就是它的值為“greeting
”的類名
。 -
掃描元素內部,我們看到文本已經更改了,因此我們需要更新實際的 DOM。
在上述三步里,只有第3步在該示例中有價值,因為程序的基本結構是沒有改變的,這其實在絕大多數的更新中都是如此。
如果我們直接跳到第3步,效率就高得多了:
if (changed.name) {
text.data = name;
}
(這幾乎就是 Svelte 生成的更新代碼了。與傳統的 UI 框架有所不同,Svelte 是一個編譯器,它可以在構建時便知悉程序中可能發生的變化,而非運行時。)
不止差異比較一個方面
React 和其他 Virtual DOM 框架使用的 diffing 算法速度都很快。
換而言之,組件本身的開銷更大。
例如你不太可能會寫出這樣的代碼……
function StrawManComponent(props) {
const value = expensivelyCalculateValue(props.foo);
return (
<p>the value is {value}</p>
);
}
如果這么干,無論 props.foo
是否已經更改,你可能會粗心地在每次更新時不小心重新計算了 value
。
不過,對於進行不必要的計算和分配,更普遍的是下面這種方式:
function MoreRealisticComponent(props) {
const [selected, setSelected] = useState(null);
return (
<div>
<p>Selected {selected ? selected.name : 'nothing'}</p>
<ul>
{props.items.map(item =>
<li>
<button onClick={() => setSelected(item)}>
{item.name}
</button>
</li>
)}
</ul>
</div>
);
}
在這里,我們每次狀態更改時,都要生成一個新的虛擬的 <li>
元素數組,每個元素都有自己內聯的事件處理程序,而不論 props.items
是否發生了變化。
除非你對性能有所不滿,否則就不會對其進行優化。這么干毫無意義,它足夠快了。
但是你想知道怎樣會更快嗎?那是在浪費時間。
其實,默認就做一些不必要的計算(即使是微不足道的),其危險之處在於你的應用最終會溫水煮青蛙般死掉,因為沒有明顯的瓶頸值得去優化。
React Hooks 會使情況變本加厲,結果可想而知。
Svelte 專門設計用來防止你陷入這種困境。
那些框架為何還要用 Virtual DOM?
關鍵是要理解:Virtual DOM 不是一種特性,而是一種手段。
它要達到的目的是支持聲明式的、狀態驅動的 UI 開發。
Virtual DOM 很有價值,因為它允許你在構建應用程序時無需考慮狀態轉換,而且性能通常_已經足夠好了_。
這意味着更少的錯誤代碼,更多的時間花在創造性的任務上,而不是在單調乏味的地方折騰。
但事實證明,我們可以在不使用 Virtual DOM 的情況下實現類似的編程模型 —— 這正是 Svelte 的用武之地。
< The End >