React 並行渲染深度實戰:useTransition/useDeferredValue/Suspense 的正確開啟方式
前端工程(更新於 2026年6月2日)
並行渲染:React 的中斷式渲染
React 18 之前是同步渲染——一旦開始渲染,無法中斷,長任務會阻塞 UI 互動。
同步渲染:
使用者輸入 → [====== 長渲染 ======] → UI 更新
↑ 使用者感覺卡頓
並行渲染:
使用者輸入 → [== 渲染 ==] → 中斷 → 處理輸入 → [== 繼續渲染 ==]
↑ 高優先級任務可以打斷低優先級
useTransition:標記低優先級更新
基礎用法
import { useTransition, useState } from 'react';
function SearchPage() {
const [query, setQuery] = useState('');
const [results, setResults] = useState([]);
const [isPending, startTransition] = useTransition();
function handleSearch(e: React.ChangeEvent<HTMLInputElement>) {
// 高優先級:立即更新輸入框
setQuery(e.target.value);
// 低優先級:搜尋結果可以延遲
startTransition(() => {
setResults(filterLargeList(e.target.value));
});
}
return (
<div>
<input value={query} onChange={handleSearch} />
{isPending && <Spinner />}
<ResultList results={results} />
</div>
);
}
實戰:大列表過濾
function ProductFilter({ products }: { products: Product[] }) {
const [filterText, setFilterText] = useState('');
const [filteredProducts, setFilteredProducts] = useState(products);
const [isPending, startTransition] = useTransition();
function handleFilterChange(text: string) {
setFilterText(text);
startTransition(() => {
// 10000 條商品過濾——低優先級
const filtered = products.filter(p =>
p.name.toLowerCase().includes(text.toLowerCase()) ||
p.category.toLowerCase().includes(text.toLowerCase()) ||
p.tags.some(t => t.includes(text))
);
setFilteredProducts(filtered);
});
}
return (
<div>
<SearchInput
value={filterText}
onChange={handleFilterChange}
// isPending 時不降低輸入框的回應性
/>
<div style={{ opacity: isPending ? 0.7 : 1, transition: 'opacity 0.2s' }}>
<ProductGrid products={filteredProducts} />
</div>
</div>
);
}
isPending 的正確用法
// ❌ 用 isPending 顯示全螢幕 loading(閃爍)
{isPending ? <FullPageSpinner /> : <Content />}
// ✅ 用 isPending 做微妙的視覺回饋
<div style={{
opacity: isPending ? 0.7 : 1,
transition: 'opacity 0.15s',
pointerEvents: isPending ? 'none' : 'auto',
}}>
<Content />
</div>
useDeferredValue:延遲版本值
與 useTransition 的關係
// useTransition 版本
const [isPending, startTransition] = useTransition();
function handleChange(value) {
setQuery(value);
startTransition(() => setDeferredQuery(value));
}
// useDeferredValue 版本(等價,更簡潔)
const [query, setQuery] = useState('');
const deferredQuery = useDeferredValue(query);
實戰:搜尋建議
function SearchWithSuggestions() {
const [query, setQuery] = useState('');
const deferredQuery = useDeferredValue(query);
return (
<div>
<SearchInput value={query} onChange={setQuery} />
{/* Suggestions 使用延遲值,不會阻塞輸入 */}
<Suggestions query={deferredQuery} />
</div>
);
}
function Suggestions({ query }: { query: string }) {
const [suggestions, setSuggestions] = useState<string[]>([]);
useEffect(() => {
if (!query) { setSuggestions([]); return; }
fetchSuggestions(query).then(setSuggestions);
}, [query]);
return (
<ul>
{suggestions.map(s => <li key={s}>{s}</li>)}
</ul>
);
}
useDeferredValue + memo = 效能利器
const deferredQuery = useDeferredValue(query);
// memo 確保只有 deferredQuery 變化時才重新渲染
const MemoizedResults = memo(Results);
return (
<div>
<SearchInput value={query} onChange={setQuery} />
<MemoizedResults query={deferredQuery} />
</div>
);
Suspense:宣告式非同步載入
基礎:載入狀態
const resource = fetchData();
function ProfilePage() {
return (
<Suspense fallback={<ProfileSkeleton />}>
<ProfileDetails resource={resource} />
<Suspense fallback={<PostsSkeleton />}>
<ProfilePosts resource={resource} />
</Suspense>
</Suspense>
);
}
function ProfileDetails({ resource }) {
// 拋出 Promise 直到資料就緒
const user = resource.user.read();
return <h1>{user.name}</h1>;
}
巢狀 Suspense:瀑布 vs 並行
// ❌ 瀑布:每個 Suspense 等前一個完成
<Suspense fallback={<Skeleton />}>
<A />
<Suspense fallback={<Skeleton />}>
<B />
<Suspense fallback={<Skeleton />}>
<C />
</Suspense>
</Suspense>
</Suspense>
// ✅ 並行:A/B/C 同時載入
<Suspense fallback={<Skeleton />}>
<A />
</Suspense>
<Suspense fallback={<Skeleton />}>
<B />
</Suspense>
<Suspense fallback={<Skeleton />}>
<C />
</Suspense>
Suspense + React Router v7
import { lazy, Suspense } from 'react';
import { Routes, Route } from 'react-router';
const Dashboard = lazy(() => import('./pages/Dashboard'));
const Settings = lazy(() => import('./pages/Settings'));
function App() {
return (
<Suspense fallback={<PageSkeleton />}>
<Routes>
<Route path="/dashboard" element={<Dashboard />} />
<Route path="/settings" element={<Settings />} />
</Routes>
</Suspense>
);
}
流式 SSR + Suspense
服務端
// app.tsx
function App() {
return (
<Layout>
<Suspense fallback={<ProductSkeleton />}>
<ProductList /> {/* 非同步資料 */}
</Suspense>
</Layout>
);
}
流式 HTML 回應
HTTP 回應流:
1. 立即傳送 Layout + ProductSkeleton
<html><body><nav>...</nav><div class="skeleton">...</div>
2. ProductList 資料就緒後,流式傳送替換
<template id="suspense-boundary">
<div class="products">...</div>
</template>
<script>
// 替換 skeleton 為真實內容
document.getElementById('suspense-boundary').replaceWith(...)
</script>
</body></html>
Next.js App Router 的流式 SSR
// app/products/page.tsx
export default async function ProductsPage() {
// 這是 Server Component,自動 Suspense
const products = await fetchProducts();
return <ProductGrid products={products} />;
}
// app/layout.tsx
export default function Layout({ children }) {
return (
<html>
<body>
<Nav />
<Suspense fallback={<Loading />}>
{children}
</Suspense>
</body>
</html>
);
}
useActionState:表單的並行處理
import { useActionState } from 'react';
async function submitForm(prevState: Result, formData: FormData): Promise<Result> {
const response = await fetch('/api/submit', {
method: 'POST',
body: formData,
});
return response.json();
}
function ContactForm() {
const [state, formAction, isPending] = useActionState(submitForm, {
status: 'idle',
});
return (
<form action={formAction}>
<input name="name" required />
<input name="email" type="email" required />
<textarea name="message" required />
<button type="submit" disabled={isPending}>
{isPending ? '提交中...' : '提交'}
</button>
{state.status === 'error' && <p className="error">{state.message}</p>}
{state.status === 'success' && <p className="success">提交成功!</p>}
</form>
);
}
常見陷阱
陷阱 1:startTransition 內的 setState 不是立即的
// ❌ 期望立即更新,但實際是延遲的
startTransition(() => {
setCount(c => c + 1);
});
console.log(count); // 還是舊值!
// ✅ 需要立即的更新放在 transition 外
setCount(c => c + 1);
陷阱 2:Suspense 邊界過粗
// ❌ 整個頁面一個 Suspense,任何資料未就緒都顯示全螢幕 loading
<Suspense fallback={<FullPageLoading />}>
<Header />
<Sidebar />
<MainContent />
<Footer />
</Suspense>
// ✅ 細粒度 Suspense,各部分獨立載入
<Header />
<Suspense fallback={<SidebarSkeleton />}>
<Sidebar />
</Suspense>
<Suspense fallback={<MainSkeleton />}>
<MainContent />
</Suspense>
<Footer />
陷阱 3:過度使用 useTransition
// ❌ 所有更新都標記為 transition(沒有必要)
startTransition(() => {
setA(value);
setB(value);
setC(value);
});
// ✅ 只對「可以等待」的更新使用 transition
setA(value); // 立即
startTransition(() => {
setB(value); // 可以延遲
setC(value); // 可以延遲
});
效能對比
| 場景 | 同步渲染 | 並行渲染 |
|---|---|---|
| 大列表過濾(10000條) | 輸入框卡頓 200ms | 輸入框流暢,列表延遲 50ms |
| 頁面切換 | 白屏等待 1s | 骨架屏 + 流式填充 |
| 並行資料載入 | 瀑布 3s | 並行 1s |
| 緊急更新打斷渲染 | 不支援 | 自動打斷 |
總結
React 並行渲染的三個核心 API 各有定位:useTransition 標記可延遲更新、useDeferredValue 建立延遲值、Suspense 宣告式非同步載入。關鍵原則:緊急更新(輸入、點選)立即處理,非緊急更新(過濾、搜尋、導航)標記為 transition。配合流式 SSR,並行渲染讓 React 應用在使用者體驗上實現質的飛躍。
本站提供瀏覽器本地工具,免註冊即可試用 →
#React#并发渲染#Suspense#useTransition#流式SSR