深入理解 React Concurrent Rendering - 流暢的 React 第七章 並行 React 筆記
React 18 引入的並行渲染(Concurrent Rendering)是 React 架構的重大革新。本文將從同步渲染的痛點出發,逐步解析並行渲染的核心機制,並探討實際應用中需要注意的問題。
同步渲染的瓶頸
在並行渲染出現之前,React 採用同步渲染(Synchronous Rendering)模式。這種模式的運作方式非常直接:一旦開始渲染,就必須一次性完成,中途無法暫停或被插隊。
這帶來了兩個關鍵問題:
阻塞主執行緒:主執行緒是瀏覽器處理使用者互動(點擊、輸入)和畫面繪製的唯一通道。當同步渲染佔用主執行緒時,所有使用者操作都會被擱置,直到渲染完成。在複雜應用中,這會導致畫面凍結、按鈕沒反應,嚴重影響使用者體驗。
缺乏優先級機制:所有任務一視同仁,沒有輕重緩急之分。後台的低優先級任務可能阻塞關鍵的使用者互動。
並行渲染 Concurrent Rendering 的核心思想
並行渲染的關鍵突破在於讓渲染過程變得「可中斷」。React 透過時間分片(Time Slicing)技術,將大型渲染任務分解成多個可在幾毫秒內完成的小區塊。
這帶來三大優勢:
- 優先級排程:根據更新的重要性和緊急性排定處理順序
- 時間分片:將渲染工作切分成小塊,分散在多個 frame 中處理
- 可中斷性:高優先級任務可以打斷低優先級任務
核心機制解析
Fiber 架構
Fiber 是 React 實現並行渲染的基礎架構。在 Fiber 出現之前,React 的 reconciliation 過程依賴 JavaScript 的原生呼叫堆疊(Call Stack),一旦開始就必須執行到底。Fiber 徹底改變了這個模式,讓 React 擁有了「自己的堆疊」,可以隨時暫停、恢復或放棄渲染工作。
1. 作為「工作單元」的虛擬堆疊幀
在 Fiber 架構中,元件樹被重新實作為一個由 Fiber 節點組成的鏈結串列(Linked List):
- 單位化工作:每個 Fiber 節點代表一個「工作單元」,就像 React 在記憶體中維護的「虛擬堆疊幀(Virtual Stack Frame)」,讓 React 不再受限於 JavaScript 原生呼叫堆疊的同步執行限制
- 攜帶元數據:每個 Fiber 物件保存了元件的類型、狀態、Props,以及指向父層(
return)、子層(child)與兄弟層(sibling)的指標,使 React 能隨時暫停並在之後精確回到該位置繼續工作
FiberNode {
type, // 元件類型
stateNode, // 對應的 DOM 節點或元件實例
pendingProps, // 新的 props
memoizedState, // 當前狀態
return, // 父 Fiber
child, // 第一個子 Fiber
sibling, // 下一個兄弟 Fiber
alternate, // 對應的另一棵樹上的 Fiber
lanes, // 優先級標記
}
2. 實現「可中斷」的渲染過程(Time-Slicing)
Fiber 將更新過程拆分為兩個階段,使渲染變得非阻塞:
渲染階段(Render Phase):非同步且可中斷。Fiber 在執行過程中不斷檢查是否超過了約 5ms 的時間切片預算,如果時間用盡,會透過 shouldYield() 讓出主執行緒給瀏覽器處理更高優先級的任務(如使用者輸入),並在下一幀恢復工作。
提交階段(Commit Phase):同步且不可中斷。一旦整個 Work-in-progress 樹準備就緒,Fiber 會一次性將變更應用到實際 DOM 上,確保 UI 的一致性。
渲染階段 Render Phase(可中斷) 提交階段 Commit Phase(不可中斷)
┌─────────────────────┐ ┌─────────────────┐
│ beginWork │ │ commitRoot │
│ ↓ │ │ ↓ │
│ 處理 Fiber 節點 │ → │ 更新 DOM │
│ ↓ │ │ ↓ │
│ shouldYield()? │ │ 執行 Effects │
│ ↓ │ └─────────────────┘
│ completeWork │
└─────────────────────┘
3. 支持「雙重緩衝」機制(Double Buffering)
Fiber 節點中的 alternate 指標讓 React 可以在記憶體中同時存在兩個版本的 Fiber 樹:
- Current 樹:目前顯示在螢幕上的樹
- Work-in-progress 樹:正在後台構建、尚未完成的更新樹
這種機制允許 React 在後台安靜地準備新 UI,不會干擾當前視圖,直到渲染完成後再快速交換指標(Swap pointers)。如果高優先級更新插入,可以直接丟棄未完成的 Work-in-progress 樹,重新開始。
Current Tree Work-in-progress Tree
A ←─ alternate ─→ A'
/ \ / \
B C B' C'(建構中)
4. 優先級調度的執行者
Fiber 與 Lanes 系統緊密配合,決定哪些工作應該先做:
- 每個 Fiber 節點的
lanes欄位標記了該節點待處理更新的優先級 - React 在遍歷 Fiber 樹時,會根據當前正在處理的
renderLanes決定是否處理該節點的更新 - 這讓 React 能像「多工處理者」一樣,優先處理點擊、打字等緊急更新,並推遲或丟棄已過時的後台任務
5. 賦能現代高級功能
沒有 Fiber 的架構支撐,以下功能將無法實現:
- Suspense:Fiber 允許 React 在等待元件載入或資料獲取時暫停子樹的渲染,展示 fallback UI,而不會鎖定整個界面
- 流式伺服器渲染(Streaming SSR):藉由 Fiber 的增量渲染能力,React 可以在伺服器端部分元件準備好後就開始流式傳輸 HTML
- 並行渲染(Concurrent Rendering):讓 React 同時準備多個版本的 UI,優化使用者體驗
調度器(Scheduler)
調度器是並行渲染的決策中樞,負責管理主執行緒的讓出(yielding)。它的核心職責包括:
- 任務排序:接收所有更新請求,決定「哪些工作現在執行」以及「哪些工作稍後執行」
- 主執行緒管理:策略性地安排任務,確保瀏覽器主執行緒不會被長時間阻塞
- 優先級協調:透過 Render Lanes 機制,根據任務緊急程度進行排序
調度器的運作機制依賴瀏覽器 API(如 setTimeout、MessageChannel)來安排任務執行時機:
- 同步任務(Sync Lane):排入微任務(microtask),在當前事件循環結束時立即處理
- 非同步任務(Non-Sync Lane):排程回調函數,等主執行緒有空檔時再處理
Render Lanes
Lanes 是 React 用來標記和組織更新優先級的位元遮罩系統。不同類型的更新會被分配到不同的 Lane:
| Lane 類型 | 超時時間 | 使用場景 |
|---|---|---|
| SyncLane / InputContinuousLane | ~250ms | 使用者點擊、輸入、手勢 |
| TransitionLane | ~5000ms | 頁面跳轉、非緊急更新 |
| IdleLane | 永不過期 | 低優先級背景任務 |
React 原始碼解析
ensureRootIsScheduled
這個函式是調度流程的入口,每當 React Root 接收到更新時就會觸發。它的職責是確保 Root 被加入調度佇列,並且有對應的微任務來處理這個調度:
1export function ensureRootIsScheduled(root: FiberRoot): void {2 // 將 root 加入調度佇列(linked list 結構)3 if (root === lastScheduledRoot || root.next !== null) {4 // 已在佇列中,跳過5 } else {6 if (lastScheduledRoot === null) {7 firstScheduledRoot = lastScheduledRoot = root;8 } else {9 lastScheduledRoot.next = root;10 lastScheduledRoot = root;11 }12 }1314 // 標記可能有同步工作待處理15 mightHavePendingSyncWork = true;1617 // 確保調度被觸發18 ensureScheduleIsScheduled();19}
scheduleTaskForRootDuringMicrotask
這個函式在微任務中執行,負責實際的調度決策:
1function scheduleTaskForRootDuringMicrotask(2 root: FiberRoot,3 currentTime: number,4): Lane {5 // 1. 標記飢餓的 lanes,防止低優先級更新被無限延遲6 markStarvedLanesAsExpired(root, currentTime);78 // 2. 決定下一批要處理的 lanes9 const nextLanes = getNextLanes(root, ...);1011 // 3. 根據 lane 類型決定調度方式12 if (includesSyncLane(nextLanes)) {13 // 同步工作:在 microtask 結束時自動執行14 root.callbackPriority = SyncLane;15 return SyncLane;16 } else {17 // 非同步工作:透過 Scheduler 排程18 const schedulerPriorityLevel = lanesToEventPriority(nextLanes);19 const newCallbackNode = scheduleCallback(20 schedulerPriorityLevel,21 performWorkOnRootViaSchedulerTask.bind(null, root),22 );23 return newCallbackPriority;24 }25}
防止飢餓機制
markStarvedLanesAsExpired 確保等待太久的更新能被強制執行:
1export function markStarvedLanesAsExpired(2 root: FiberRoot,3 currentTime: number,4): void {5 const pendingLanes = root.pendingLanes;6 const expirationTimes = root.expirationTimes;78 let lanes = pendingLanes;9 while (lanes > 0) {10 const index = pickArbitraryLaneIndex(lanes);11 const lane = 1 << index;12 const expirationTime = expirationTimes[index];1314 if (expirationTime === NoTimestamp) {15 // 尚未設定過期時間,計算並設定16 expirationTimes[index] = computeExpirationTime(lane, currentTime);17 } else if (expirationTime <= currentTime) {18 // 已過期,標記為 expired 強制執行19 root.expiredLanes |= lane;20 }2122 lanes &= ~lane;23 }24}
調度流程圖

實際場景分析
場景 1:使用者點擊按鈕
onClick={() => setState(x)}
→ ensureRootIsScheduled(root)
→ includesSyncLane(nextLanes) = true
→ 不額外排程,等 microtask 結束時同步執行
場景 2:startTransition 更新
startTransition(() => setState(x))
→ ensureRootIsScheduled(root)
→ nextLanes = TransitionLane
→ scheduleCallback(NormalSchedulerPriority, performWork)
→ 可被中斷的非同步渲染
場景 3:Suspense 等待數據
render(<Suspense><AsyncComponent /></Suspense>)
→ isWorkLoopSuspendedOnData() = true
→ 取消 callback,等待 ping 或新的更新
並行渲染 Hooks
useTransition
useTransition 將狀態更新標記為「非緊急」,適用於可能導致畫面卡頓的操作(如頁面跳轉、篩選大量資料)。
1const [isPending, startTransition] = useTransition();23// isPending:指示非緊急更新是否進行中,可用於顯示 loading 狀態4// startTransition:包裹耗時的狀態更新56function handleFilter(value) {7 // 輸入框立即響應8 setInputValue(value);910 // 篩選結果可以延遲,不阻塞輸入11 startTransition(() => {12 setFilteredList(computeExpensiveFilter(value));13 });14}
實際應用:在即時聊天室中,當伺服器不斷傳來新訊息時,用 startTransition 包裹 setMessages 操作,確保使用者打字的流暢度優先於訊息列表的更新。
useTransition 的 Lane 調度流程
useTransition 完整狀態機

實際場景:Tab 切換
從以上的例子中可以發現,使用 useTransition 讓大型的 UI 更新不會阻塞使用者互動,同時透過 isPending 提供即時的視覺反饋,並且在使用者改變心意時能夠立即響應而不必等待前一個渲染完成。
useDeferredValue
useDeferredValue 延遲更新 UI 的某個部分,讓舊內容多顯示一下,直到新內容準備好。相較於 debounce 和 throttle,它的優勢在於「可被中斷」—— 如果使用者在延遲期間輸入了新內容,React 會放棄舊的渲染。
1function SearchResults({ query }) {2 // deferredQuery 會「落後」於 query3 const deferredQuery = useDeferredValue(query);45 // 輸入框始終保持流暢6 // 搜尋結果在系統有餘裕時才更新7 return ;8}
useDeferredValue 內部機制

這就是 useDeferredValue 能夠比 debounce/throttle 更有效的原因:它不是簡單地「延遲固定時間」,而是透過 Lane 優先級系統,讓 React 自動決定何時處理延遲的更新,並且在有新輸入時能夠主動中斷舊的渲染工作。
useDeferredValue 的 Lane 調度流程
畫面撕裂(Tearing)問題
畫面撕裂是並行渲染獨有的副作用。想像這個情境:React 正在渲染一個列表,渲染到一半時被暫停。如果暫停期間外部資料源發生變化,恢復渲染時後續元件就會讀取到新資料,導致同一畫面上的元件顯示了來自不同時間點的資料。
Tearing 問題示意
useSyncExternalStore
這個 Hook 專門解決 Tearing 問題:
1const state = useSyncExternalStore(2 subscribe, // 訂閱外部資料源3 getSnapshot, // 取得當前快照4 getServerSnapshot // SSR 用的快照5);
運作原理:
- 訂閱外部資料:監聽資料源變化
- 取得快照:在渲染時取得資料的一致性快照
- 強制同步更新:資料變化時觸發同步更新,確保所有元件一起重新渲染
名稱中的 "Sync" 具有雙重含義:透過強制執行同步(synchronous)更新,達成將 UI 與外部資料同步化(synchronize)的目的。
useSyncExternalStore 如何解決 Tearing
完整使用範例

1// 實際使用範例:訂閱瀏覽器視窗寬度2function useWindowWidth() {3 return useSyncExternalStore(4 // subscribe:訂閱 resize 事件5 (callback) => {6 window.addEventListener('resize', callback);7 return () => window.removeEventListener('resize', callback);8 },9 // getSnapshot:取得當前寬度10 () => window.innerWidth,11 // getServerSnapshot:SSR 時的預設值12 () => 102413 );14}1516// 使用17function ResponsiveComponent() {18 const width = useWindowWidth();19 return 視窗寬度:{width}px;20}
這就是為什麼當你使用 Redux、Zustand 等外部狀態管理時,它們內部都會使用 useSyncExternalStore 來確保在並行渲染環境下不會出現 Tearing 問題。
總結
並行渲染讓 React 從一口氣渲染完進化到「可中斷、可排程」的模式。理解 Fiber、Scheduler 和 Lanes 的協作機制,能幫助我們更好地運用 useTransition、useDeferredValue 等 API 來優化應用效能,同時也要注意 Tearing 等潛在問題並使用 useSyncExternalStore 來解決。
延伸閱讀
- React Source Code - ReactFiberRootScheduler.js
- React Source Code - ReactFiberLane.js
- 流暢的React 第七章:並行 React