React 18 引入了並發功能,從根本上改變了渲染的工作方式。這些功能使 React 可以一次準備多個版本的 UI、中斷正在進行的工作,並優先考慮緊急更新而不是非緊急更新。結果是應用程式回應速度更快,而無需放棄使 React 高效的聲明式程式設計模型。
React 中的並發不是一種全有或全無的模式。與早期實驗版本中廢棄的「並發模式」概念不同,React 18+ 選擇加入並發功能。您可以逐一採用它們,因為它們可以提供最大的價值。底層 Fiber 架構使這成為可能:React 的渲染階段可以暫停和恢復,因此協調器可以隨著優先順序的變化在不同的工作單元之間切換。
使用轉換鉤子
useTransition 掛鉤可讓您將狀態更新標記為非緊急。這可以防止更新在關鍵用戶互動(例如打字)期間阻塞主執行緒。
import { useTransition } from "react";
function SearchPage() {
const [isPending, startTransition] = useTransition();
const [query, setQuery] = useState("");
function handleChange(e) {
// Urgent: update the input value immediately
const value = e.target.value;
setQuery(value);
// Non-urgent: filter the large dataset
startTransition(() => {
setFilter(value);
});
}
return (
<div>
<input value={query} onChange={handleChange} />
{isPending && <Spinner />}
<SearchResults filter={filter} />
</div>
);
}
isPending 標誌提供了一種在不阻塞 UI 的情況下顯示載入指示器的方法。當 React 正在進行轉換時,isPending 是 true,使用者可以繼續鍵入,因為轉換可能會被更緊急的狀態更新中斷。
轉換與使用 setTimeout 進行去抖動有根本的不同。轉換根本不會延遲執行。它降低了渲染工作的優先級,允許 React 暫停它,檢查更高優先級的工作,並在執行緒空閒時恢復。去抖動總是會引入固定的延遲;當 CPU 空閒時,轉換會引入零延遲。
useDeferredValue 鉤子
其中 useTransition 包裝狀態設定器, useDeferredValue 包裝值。當您無法輕鬆包裝狀態更新本身但仍希望保持 UI 回應時,這非常有用。
import { useDeferredValue, useMemo } from "react";
function SearchResults({ query, data }) {
const deferredQuery = useDeferredValue(query);
const isStale = query !== deferredQuery;
const filtered = useMemo(
() => expensiveFilter(data, deferredQuery),
[deferredQuery, data]
);
return (
<div style={{ opacity: isStale ? 0.5 : 1 }}>
{filtered.map((item) => (
<ListItem key={item.id} item={item} />
))}
</div>
);
}
當 CPU 閒置時,延遲值會立即更新,而不會在固定延遲之後更新。這會建立一個「陳舊狀態」模式:使用者在計算新結果時看到先前的結果(稍微變暗),而不是空白的載入旋轉器。這種模式提供了一種感覺上更流暢的體驗,因為 UI 永遠不會變空。
| 方法 | 延遲 | 使用者體驗 |
|---|---|---|
| 同步渲染 | 每次擊鍵都充滿 | UI 在過濾期間凍結 |
| 去抖動(300 毫秒) | 固定 300 毫秒延遲 | 輸入感覺延遲 |
| 佔位符_0 | 空閒時零延遲 | 反應靈敏輸入,平滑過渡 |
並發渲染的懸念
在並發渲染下,懸念變得更加強大。在同步模型中,當元件掛起時,Suspense 邊界上方的整個樹都會回落到載入狀態。透過並發功能,同級組件在解析掛起的組件時保持互動。
function Page() {
return (
<Suspense fallback={<PageSkeleton />}>
<Sidebar />
<Suspense fallback={<DetailSkeleton />}>
<MainContent />
</Suspense>
</Suspense>
);
}
嵌套 Suspense 邊界可讓您控制載入狀態的粒度。 Sidebar 可以在 MainContent 流入時立即渲染。與 startTransition 結合使用,您可以在載入下一個畫面時顯示上一個 UI,從而消除困擾傳統導覽的「載入閃現」。
startTransition 外部鉤子
startTransition 函數也可以作為獨立導入在 React 元件之外使用。這對於狀態管理庫、路由器事件處理程序以及任何鉤子不可用的程式碼路徑都很有價值。
import { startTransition } from "react";
import { useNavigate } from "react-router-dom";
function navigateWithTransition(path) {
startTransition(() => {
// Wrap the navigation in a transition
// so the previous view stays visible
navigate(path);
});
}
您可以將 Zustand 或 Redux 儲存更新包裝在 startTransition 中,以降低非緊急狀態變更的優先權。當儲存更新觸發許多訂閱者之間昂貴的衍生運算時,這尤其有用。
建立響應式搜尋元件
實用的搜尋元件將所有並發功能組合在一起。輸入狀態緊急。過濾器計算使用 useDeferredValue。導覽至詳細資料頁面使用路由器上的 startTransition 。搜尋結果本身被包裹在懸念邊界。
以下是一個完整的模式:
function SearchContainer() {
const [input, setInput] = useState("");
const deferredInput = useDeferredValue(input);
function handleClear() {
setInput("");
// The deferred value will catch up naturally
}
return (
<Suspense fallback={<ResultsSkeleton />}>
<SearchInput
value={input}
onChange={setInput}
onClear={handleClear}
/>
<SearchResults query={deferredInput} />
</Suspense>
);
}
當使用者快速鍵入時,只有最後一次擊鍵才會觸發完整渲染。所有中間擊鍵都會中斷先前的轉換。這避免了“卡頓搜尋”問題,即打字感覺緩慢,因為每次擊鍵都會觸發昂貴的過濾器通過。
高級模式和最佳實踐
對於分層優先權控制,請將 useTransition 與 useDeferredValue 組合。使用 useTransition 進行導覽層級狀態變更(選項卡切換、頁面轉換),使用 useDeferredValue 進行視圖內的衍生計算。
若要測試並發功能,請使用帶有 act 包裝器的 React 測試庫:
import { act, render, screen } from "@testing-library/react";
await act(async () => {
render(<SearchPage />);
});
// Assert on the pending state
expect(screen.getByText("Loading...")).toBeInTheDocument();
優化前的設定檔。 React DevTools Profiler 顯示渲染是否中斷以及每個渲染階段消耗了多少時間。過早使用並發功能會增加複雜性,而不會帶來可衡量的好處 - 始終首先進行測量,然後在解決真正瓶頸的地方應用轉換和延遲值。
相容性和遷移
並發功能需要 React 18+ 並且完全可以選擇加入。升級時不會破壞現有組件。建議的遷移路徑從高延遲使用者互動開始:搜尋輸入、過濾大型清單和選項卡切換。一旦這些順利進行,就擴展到導航轉換和資料載入模式。
一些同步改變狀態的第三方函式庫可能不尊重轉換邊界。在這些上下文中採用轉換之前測試庫相容性。
概括
並發功能為您提供了三個強大的工具:用於取消狀態更新優先級的 useTransition、用於推遲派生計算的 useDeferredValue 以及用於優雅加載狀態的並發 Suspense。每個解決特定類別的回應問題。首先進行分析,選擇正確的工具,並提供即使在繁重的計算下也感覺即時的 UI。

