什麼是 Virtual DOM? 從底層原理到 React 的渲染策略
2025-07-08 03:21:48
## 為什麼要談 Virtual DOM?
每當我們在網頁上看到動態更新的內容,背後都可能涉及了對 DOM (文件物件模型) 的操作。然而,DOM 與瀏覽器的渲染引擎是緊密耦合的,任何一次看似微小的改動,都可能觸發瀏覽器進行成本高昂的 重排 (Reflow) 與 重繪 (Repaint),頻繁的操作最終會成為應用的效能瓶頸。
為了在提供豐富互動的同時,又能維持流暢的體驗,前端框架必須找到一個方法來「最小化對真實 DOM 的操作」。而 Virtual DOM,正是為此而生的一種優雅策略。
## DOM 是什麼?為什麼慢?
很間單的先用一句話說明是:瀏覽器呈現 UI 的載體。但在我整理資料之後我覺得可以用更精細的說明:
1. 網頁瀏覽器內部的 HTML 解析器(parsing)會將原始的 HTML/XML 文本解析並轉換為一個樹狀的資料結構,這就是 Document Object Model (DOM)。
2. DOM 的每個節點都代表文件中的一部分,並以 JavaScript 物件的形式存在,提供了一系列屬性與方法 (DOM API)。
3. JavaScript 正是透過這些 DOM API 來存取、查詢、修改這些節點,從而動態地控制網頁的內容、樣式和行為,實現使用者互動。
### 什麼是 Reflow / Repaint?
- **回流 (Reflow)**:當 DOM 元素的幾何屬性(如寬度、高度、位置)發生改變,導致瀏覽器需要重新計算元素在頁面上的佈局時,就會觸發回流。
- **重繪 (Repaint)**:當元素的外觀屬性(如顏色、背景)發生改變,但不影響其佈局時,瀏覽器只需重新繪製該部分的外觀,這個過程稱為重繪。
- 回流必定會觸發重繪,但重繪不一定會觸發回流。因此,回流對效能的影響遠大於重繪。
### 每次 DOM 變化都可能觸發效能瓶頸
瀏覽器在操作 DOM 的過程中都有可能引發 Reflow 或 Repaint,過程中是仰賴 瀏覽器CPU 需要重新計算,在反覆的過程中是可能引起效能瓶頸的。

☝️DOM Tree + 瀏覽器渲染流程
## Virtual DOM 是什麼?
Virtual DOM 是一個存於記憶體中的 JavaScript 物件,它用來描述真實 DOM 的結構。你可以把它想像成一張 UI 的「設計藍圖」。
這張「藍圖」記錄了每個節點的類型 (例如 div)、屬性 (例如 className: 'title') 以及它的子節點。當應用程式的狀態改變時,框架會建立一張新的「藍圖」,並比較新舊藍圖的差異,然後只針對真正有變動的部分去更新真實的 DOM。這個過程避免了直接且頻繁地操作笨重的真實 DOM。
### 真實 DOM 結構 vs. JavaScript 表示的 Virtual DOM
```html
<div class="container">
<h1 id="title">Hello</h1>
<p>Virtual DOM</p>
</div>
```
↕️
```javascript
// 這是 Virtual DOM 的樣子
// (一個巢狀的 JavaScript 物件)
const vdom = {
tagName: 'div',
props: {
className: 'container'
},
children: [
{
tagName: 'h1',
props: {
id: 'title'
},
children: ['Hello']
},
{
tagName: 'p',
props: {},
children: ['Virtual DOM']
}
]
};
```
## Virtual DOM 的核心機制
Virtual DOM 的威力展現在UI 更新的生命週期中。我們可以將整個過程拆解成兩大階段:
- 階段一:首次渲染 (Initial Render) 當頁面首次載入時,框架會根據初始狀態建立第一版的 Virtual DOM。接著,它會以此為藍圖,完整地渲染出真實的 DOM 結構,這就是我們在畫面上看到的樣子。
- 階段二:更新與協調 (Update & Reconciliation) 當應用程式的狀態改變時 (例如,使用者點擊按鈕),神奇的事情發生了:
1. 產生新樹 (Generate): 框架會根據新的狀態,在記憶體中建立一棵全新的 Virtual DOM Tree。
2. 比較差異 (Diff): 接著,最關鍵的 Diffing (差異比對) 演算法會上場。它會高效地比較「新的 Virtual DOM」和「更新前的舊 Virtual DOM」,找出兩者之間最小的差異點 (例如:一個節點被新增、一個屬性被修改)。
3. 批次更新 (Patch): 框架會將這些差異彙整成一個「補丁」,然後只針對這些變動的部分去執行最小化的真實 DOM 操作。這個過程稱為 Patch。
透過這套「在記憶體中計算差異,再一次性更新真實 DOM」的機制,Virtual DOM 大幅減少了直接操作 DOM 的次數,從而避免了昂貴的 Reflow 和 Repaint,提升了應用程式的效能。

### Virtual DOM 的核心思想:
在記憶體中完成比較,然後只對真實 DOM 進行最小化的必要修改。 看圖的人可以立刻明白,整個 DOM 並沒有被重新創建,只有黃色那一塊被「Patch」上去了。
## Virtual DOM 的優勢與限制
### 優點:
1. **快**:
將複雜且耗時的 DOM 比對操作,從相對緩慢的瀏覽器渲染引擎層,完全轉移到了高速的 JavaScript 引擎中。在記憶體裡比較兩個 JavaScript 物件的差異,遠比直接查詢、操作真實 DOM 樹來得快。
2. **可以找出最小操作**:
這是 Virtual DOM 最核心的價值。透過 Diff 演算法,它能精準計算出「真正需要改變」的最小集合,從而避免了大量不必要的 Reflow 與 Repaint,將瀏覽器的效能浪費降到最低。
3. 提升開發體驗與抽象化:
Virtual DOM 讓開發者可以從繁瑣的 DOM 操作中解放出來。我們只需要以「宣告式」的方式描述 UI 應該長什麼樣子,而不用去關心具體的 DOM 操作步驟。框架會自動處理中間所有的髒活。
4. **批次更新與跨平台潛力**:
因為所有的變動都會先在 Virtual DOM 層計算,框架可以將一段時間內的多個狀態變更「批次處理」(Batching),再一次性更新到真實 DOM,進一步提升效能。同時,因為 Virtual DOM 本身是一個獨立於瀏覽器的 JavaScript 物件,這也為跨平台渲染(例如在 React Native 中渲染成原生元件)提供了可能性。
### 缺點:
1. **記憶體開銷**:
在記憶體中維護一個完整的 DOM 複製品,無疑會佔用更多的記憶體。對於一些極端輕量或記憶體受限的應用場景,這可能會成為一個考量點。
2. **並非總是更快**:
Virtual DOM 的優勢體現在「頻繁且複雜」的 UI 更新上。對於首次渲染,它多了一層計算,理論上會比直接產生 DOM 慢一點。對於那些只有極少量更新的簡單頁面,直接操作 DOM 的成本可能更低。
3. **演算法成本與 `key`**:
Diff 演算法本身雖然高效,但依然有其計算成本。尤其是在處理列表時,如果沒有為每個子元素提供一個穩定且唯一的 `key`,React 可能無法有效複用節點,甚至會採用效率更低的策略(例如:銷毀整個列表再重建),導致效能不升反降,這是在開發中必須注意的陷阱。
## 延伸觀點:React 如何駕馭 Virtual DOM?
前面我們探討了 Virtual DOM 的通用概念,現在讓我們聚焦於 React 是如何將這個概念變成一個強大、高效的渲染策略。
### 從 JSX 到 React Element:藍圖的繪製
在 React 中,構成 Virtual DOM 的最小單位被稱為 React Element。它就是一個輕量的 JavaScript 物件,用來描述你想在畫面上看到什麼。
React 提供了 createElement 函式來產生這些物件。
```javascript
const element = createElement(type, props, ...children)
/**
* 參數分別對應:元素類型、屬性、子元素資訊
*/
```
例如,要描述一個按鈕,程式碼如下:
```javascript
import { createElement } from 'react';
const buttonElement = createElement(
'button',
{ className: 'primary' },
'Click Me!'
);
```
顯然,如果整個應用程式都這樣寫,程式碼會變得非常難以閱讀。為此,React 引入了 JSX。它是一種語法糖,讓我們能用類似 HTML 的語法來「宣告」UI,編譯後會自動轉換為 createElement 的呼叫。
> 重點:JSX 並不是 HTML,它是一種更直觀的、用來建立 React Element 的語法。
透過 JSX 表達一顆按鈕 virtual DOM:
```jsx
const buttonElement =
<button className="primary">
Click Me!
</button>;
// ...會被轉譯成上面那段 React.createElement(...) 的程式碼。
```
### Reconciliation:框架更新畫面的藝術
當框架需要根據新的狀態藍圖(Virtual DOM)來更新實際的 DOM 時,這個「使兩者保持同步」的過程,廣義上可以稱為 Reconciliation (協調)。
這並不是 React 獨有的概念。 許多採用 Virtual DOM 的框架(例如 Vue)也都有自己的協調機制,只是具體的實現細節和演算法會有所不同。
不過,「Reconciliation」這個詞因為 React 官方文件的大量使用而廣為人知。在 React 的世界裡,它特指其內部一套包含 Diffing 演算法 和 Commit 流程 的完整畫面更新策略。接下來,我們就來看看 React 是如何實現它的 Reconciliation 的。
當我們透過 setState 觸發更新時,React 會啟動一個名為 Reconciliation (協調) 的過程,這正是 React 更新畫面的核心機制。這個過程可以被簡化為兩個主要階段:
**1. Render Phase (渲染階段):**
- 觸發: 當狀態改變,React 會重新執行相關的組件函式。
- 工作: 產生一棵新的 React Element Tree (新的 Virtual DOM)。
- 比對: 在這個階段,React 會執行 Diffing 演算法,比較新舊兩棵樹,找出所有差異點。
- 這個階段是純粹的計算,可以被暫停或中斷,且不會產生任何副作用(不會修改真實 DOM)。
**2. Commit Phase (提交階段):**
- 觸發: Render Phase 完成後,React 會得到一份包含所有變更的「清單」。
- 工作: React 會一次性地將這些變更提交 (Commit) 到真實 DOM 上,執行所有必要的 DOM 新增、刪除和更新操作。這個過程就是我們所說的 Patching。
- 這個階段會直接操作 DOM,因此是同步執行的,不能被打斷。
React 正是透過 JSX 讓我們能輕鬆地描述 Virtual DOM,再透過 Render Phase 在記憶體中高效地計算出差異,最後在 Commit Phase 一次性地完成最小化的真實 DOM 更新。
## 總結
**什麼是 Virtual DOM?**
它是一種為了解決「直接操作 DOM 成本過高」而生的效能優化模式。其核心思想是,在 JavaScript 記憶體中建立一個輕量的 DOM 結構「藍圖」,當狀態變更時,透過高效的 Diff (比對) 演算法計算出最小差異,再將這些差異一次性 Patch (更新) 到真實 DOM 上,從而避免了頻繁且昂貴的瀏覽器重排與重繪。
**React 是如何運用 Virtual DOM?**
React 是將此模式發揚光大的實踐者。它透過 JSX 作為語法糖,讓開發者能宣告式地定義 UI,最終生成構成 Virtual DOM 的 React Element。其核心的 Reconciliation (協調) 過程被分為兩個階段:在 Render Phase 中計算差異,並在 Commit Phase 中將變更應用至 DOM,這套精密的機制正是 React 高效渲染的基石。
## 參考資料
1. [Document Object Model
](https://en.wikipedia.org/wiki/Document_Object_Model)
2. [回流 (Reflow) 和重繪 (Repaint) 是什麼?以及如何優化?](https://www.explainthis.io/zh-hant/swe/repaint-and-reflow)
3. [Virtual DOM & V-Node](https://ithelp.ithome.com.tw/articles/10193220?sc=iThelpR)
4. [Rendering Mechanism](https://vuejs.org/guide/extras/rendering-mechanism.html)
5. [Framework main features](https://developer.mozilla.org/en-US/docs/Learn_web_development/Core/Frameworks_libraries/Main_features)
6. [createElement](https://react.dev/reference/react/createElement)
7. [ React 畫面更新的核心機制(上):一律重繪渲染策略](https://ithelp.ithome.com.tw/articles/10298007)
8. [React 畫面更新的核心機制(下):Reconciliation](https://ithelp.ithome.com.tw/articles/10298053)
點擊複製文章連結