React 效能 是做什麼的?
React 18/19 和 Next.js 的效能優化模式,改編自 Vercel Labs react-best-practices (MIT, v1.0.0)。此技能按優先級組織規則,並為主動式程式碼審查和重構提供決策樹指導。
何時啟用
- 撰寫或審查 React/Next.js 效能相關程式碼
- 診斷頁面載入緩慢、互動卡頓或客戶端 CPU 使用率過高
- 審計套裝大小或 Lighthouse Core Web Vitals 回歸
- 消除 Server Components / API 路由中的瀑布式請求
- 減少客戶端重新渲染
- 優化長列表、動畫或水合 (hydration)
- 審查涉及
app/、pages/、components/或資料層的 PR 中的優化選擇
優先級索引
| 優先級 | 類別 | 前綴 | 適用時機 |
|---|---|---|---|
| 1 — 關鍵 | 消除瀑布式程式請求 | async- | 任何 await 後面跟著獨立的 await |
| 2 — 關鍵 | 套裝大小優化 | bundle- | 首次載入 JS、路由層級匯入、第三方套件庫 |
| 3 — 高 | 伺服器端效能 | server- | RSC、Server Actions、API 路由、SSR |
| 4 — 中高 | 客戶端資料擷取 | client- | SWR / TanStack Query / hook 中的原始 fetch |
| 5 — 中 | 重新渲染優化 | rerender- | 高頻率狀態更新、父子元件扇出 |
| 6 — 中 | 渲染效能 | rendering- | 長列表、動畫、水合 |
| 7 — 低中 | JavaScript 效能 | js- | 熱門迴圈、頻繁的記憶體分配 |
| 8 — 低 | 進階模式 | advanced- | Effect-Event 整合、穩定的 ref |
1. 消除瀑布式請求 (關鍵)
「瀑布式請求是 #1 的效能殺手」— 每個連續的
await都會增加完整的網路延遲。
在 await 之前先檢查廉價條件
在等待遠端資料之前,先檢查同步條件 (props、環境變數、寫死的旗標)。
// 不正確
async function Page({ id }: { id: string }) {
const flag = await getFlag("show-page");
if (!flag || !id) return null;
const data = await getData(id);
// ...
}
// 正確 — 先用便宜的同步條件短路
async function Page({ id }: { id: string }) {
if (!id) return null;
const flag = await getFlag("show-page");
if (!flag) return null;
const data = await getData(id);
}
將 await 延遲到使用時
將 await 移動到使用它的分支中。
// 不正確 — 在決定是否需要資料之前就進行 await
const user = await getUser(id);
if (mode === "guest") return renderGuest();
return renderUser(user);
// 正確
if (mode === "guest") return renderGuest();
const user = await getUser(id);
return renderUser(user);
對獨立工作使用 Promise.all
// 不正確 — 順序執行
const user = await getUser(id);
const posts = await getPosts(id);
const followers = await getFollowers(id);
// 正確 — 平行執行
const [user, posts, followers] = await Promise.all([
getUser(id),
getPosts(id),
getFollowers(id),
]);
部分依賴 — 提早開始,延遲 await
// 正確 — 啟動所有 Promise,僅在需要結果時才 await
const userP = getUser(id);
const postsP = getPosts(id);
const profile = await getProfile(id);
if (profile.private) return null;
const [user, posts] = await Promise.all([userP, postsP]);
用 Suspense 實現串流
將 <Suspense> 邊界推近資料來源,以便頁面在較慢的子樹串流進來時,能先渲染出可顯示的內容。取捨:當內容到達時可能產生版面位移 — 需預留空間 (骨架或 min-height)。
Server Components:透過組合實現平行處理
// 不正確 — 同一個元件內的兄弟 await 會順序執行
export default async function Page() {
const user = await getUser();
const cart = await getCart();
return <View user={user} cart={cart} />;
}
// 正確 — 拆分為子元件,React 會平行執行它們
export default async function Page() {
return (
<View>
<UserSection />
<CartSection />
</View>
);
}
2. 套裝大小優化 (關鍵)
直接匯入,避免使用 Barrel
Barrel index.ts 檔案會強制打包工具遍歷整個模組圖,即使 tree-shaking 移除了大部分內容。在許多真實應用程式中,直接匯入可節省 200-800ms 的首次載入 JS。
// 不正確
import { Button, Card, Modal } from "@/components";
// 正確
import { Button } from "@/components/Button";
import { Card } from "@/components/Card";
import { Modal } from "@/components/Modal";
Next.js 13.5+ 有 Optimize Package Imports 可以自動化列出套件的此操作 — 請使用它;對於未列出的套件庫,仍需要手動直接匯入。
靜態可分析路徑
// 不正確 — 破壞打包工具/追蹤分析
const mod = await import(`./pages/${name}`);
// 正確 — 每個分支明確指定
const mod = name === "home" ? await import("./pages/home") : await import("./pages/about");
為重型元件使用動態匯入
import dynamic from "next/dynamic";
const HeavyChart = dynamic(() => import("./HeavyChart"), {
loading: () => <Skeleton />,
ssr: false, // 僅限客戶端時
});
延遲載入第三方腳本
分析、日誌記錄、支援小工具等應在水合 之後 載入。使用 next/script 搭配 strategy="afterInteractive" (預設) 或 "lazyOnload"。
條件式模組載入
if (user.role === "admin") {
const { AdminPanel } = await import("./admin/AdminPanel");
// ...
}
在 hover/focus 時預先載入
在 hover 時觸發 <link rel="preload"> 或 import(),以便在使用者點擊時,套裝已存在於快取中。
3. 伺服器端效能 (高)
像 API 路由一樣驗證 Server Actions
每個 "use server" 函數都是一個公開端點。必須在 action 內部進行驗證 與 授權 — 永遠不要依賴呼叫它的客戶端元件來把關。
"use server";
export async function deleteUser(formData: FormData) {
const session = await getSession();
if (!session?.user) throw new Error("Unauthorized");
const targetId = String(formData.get("id"));
if (session.user.role !== "admin" && session.user.id !== targetId) {
throw new Error("Forbidden");
}
await db.user.delete({ where: { id: targetId } });
}
使用 React.cache() 實現每個請求的去重
import { cache } from "react";
export const getUser = cache(async (id: string) => {
return db.user.findUnique({ where: { id } });
});
React.cache 在單個請求內進行去重。從三個不同的 Server Components 中呼叫 getUser("1"),在相同的渲染過程中,只會執行一次資料庫查詢。
為跨請求資料使用 LRU 快取
對於 不會 因請求而改變的資料 (配置、查詢表),請在 React 外部使用 LRU 快取或 unstable_cache 進行快取。
避免在 RSC props 中重複序列化
當一個 Server Component 將相同的資料渲染到多個 Client Components 時,該資料會為每個消費者序列化一次。請將 Client Component 往上提,並傳遞 children。
將靜態 I/O 提升到模組作用域
// 正確 — 在模組載入時執行一次
const fontData = readFileSync(fontPath);
export async function Page() {
return <Banner font={fontData} />;
}
不要在 RSC/SSR 中使用可變的模組級狀態
伺服器上的模組狀態在所有請求之間共享 — 這會造成使用者之間的競爭條件。請使用請求作用域的儲存 (headers()、cookies()、非同步上下文) 來替代。
最小化傳遞給 Client Components 的資料
只序列化 Client 需要的資料。在資料庫層級進行欄位篩選、分頁、投影。
使用每個項目的 Promise.all 來平行化巢狀擷取
const users = await getUsers();
const enriched = await Promise.all(
users.map(async (u) => ({ ...u, posts: await getPostsFor(u.id) })),
);
使用 after() 處理非阻塞工作
Next.js 15 的 after() 在回應發送後執行工作 — 例如日誌記錄、快取預熱、分析。
import { after } from "next/server";
export async function GET() {
const data = await getData();
after(() => logAnalytics(data));
return Response.json(data);
}
4. 客戶端資料擷取 (中高)
使用 SWR / TanStack Query 實現去重
多個元件呼叫 useUser(id) 應該共享一個網路請求和一個快取條目。請使用 SWR 或 TanStack Query — 永遠不要為了共享資料而自己實作 useEffect + fetch。
去重全域事件監聽器
// 不正確 — 每個元件都添加自己的
useEffect(() => {
window.addEventListener("scroll", handler);
return () => window.removeEventListener("scroll", handler);
}, []);
// 正確 — 透過一個 hook + 全域 subject 實現單一共享監聽器
const useScroll = createScrollHook(); // 底層是單例 subject
為捲動事件使用 Passive 監聽器
window.addEventListener("scroll", handler, { passive: true });
改善捲動流暢度;監聽器無法呼叫 preventDefault()。
localStorage:版本化 + 最小化
- 始終儲存一個
version欄位;在結構變更時提升版本號,並遷移或丟棄舊資料 - 保持 payload 小 —
localStorage是同步的,會阻塞主執行緒
5. 重新渲染優化 (中)
不要訂閱僅在回呼中使用的狀態
// 不正確 — 每次 count 變化時都會重新渲染
const count = useStore((s) => s.count);
const handler = () => doSomething(count);
// 正確 — 在呼叫時讀取一次
const handler = () => {
const count = useStore.getState().count;
doSomething(count);
};
將昂貴的工作提取到 memoized 元件中
// 正確 — 子元件僅在 `items` 變化時重新渲染
const Heavy = memo(function Heavy({ items }: { items: Item[] }) {
return <Chart data={transform(items)} />;
});
提升預設的非基本類型 props
// 不正確 — 每次渲染都建立新陣列,破壞 memo
<List items={items ?? []} />
// 正確
const EMPTY: Item[] = [];
<List items={items ?? EMPTY} />
在 effects 中使用基本類型依賴
// 不正確 — 每次渲染都建立新的物件識別
useEffect(() => {}, [{ id, name }]);
// 正確 — 基本類型
useEffect(() => {}, [id, name]);
訂閱衍生的布林值,而不是原始值
// 不正確 — 任何購物車變更都會重新渲染
const cart = useStore((s) => s.cart);
const hasItems = cart.length > 0;
// 正確 — 僅在空/非空狀態切換時重新渲染
const hasItems = useStore((s) => s.cart.length > 0);
在渲染期間推導,永遠不要透過 useEffect
// 不正確
const [full, setFull] = useState("");
useEffect(() => setFull(`${first} ${last}`), [first, last]);
// 正確
const full = `${first} ${last}`;
使用函數式 setState 以獲得穩定的回呼
// 正確
const increment = useCallback(() => setCount((c) => c + 1), []);
為昂貴的值使用懶初始化 state
const [tree] = useState(() => parseTree(largeInput));
避免為簡單的基本類型使用 memo
useMemo(() => x + 1, [x]) 是開銷。Memo 在物件識別和昂貴計算上才值得使用。
拆分具有獨立依賴的 hooks
// 不正確 — 如果任一來源發生變化,兩個 selector 都會重新執行
const { a, b } = useSomething(source1, source2);
// 正確
const a = useA(source1);
const b = useB(source2);
將互動邏輯移入事件處理器
事件處理器僅在使用者操作時執行 — 而 useEffect 會在依賴發生變化時重新執行。
為非緊急更新使用 startTransition
const [pending, startTransition] = useTransition();
startTransition(() => setFilters(newFilters));
為昂貴的渲染使用 useDeferredValue
const deferredQuery = useDeferredValue(query);
const results = useMemo(() => expensiveSearch(deferredQuery), [deferredQuery]);
為瞬態高頻值使用 useRef
適用於經常變化但不應觸發重新渲染的值 (時間戳、最後按鍵、累加器)。
不要在元件內部定義元件
// 不正確 — Inner 在每次 Outer 渲染時都是一個新元件
function Outer() {
const Inner = () => <span />;
return <Inner />;
}
每次渲染都會創建一個新的 Inner 類型,這會破壞比對 (reconciliation) 並卸載子元素。
6. 渲染效能 (中)
動畫化包裝層,而不是 SVG
對 SVG 外層的 <div> 包裝進行變換 (transform) 是 GPU 加速的;直接變換 SVG 本身則會觸發繪製 (paint)。
為長列表使用 content-visibility: auto
.row { content-visibility: auto; contain-intrinsic-size: auto 80px; }
瀏覽器會跳過螢幕外的渲染 — 對於有數百行的列表來說,這是一項重大改進。
提升靜態 JSX
const STATIC_HEADER = <h1>Title</h1>;
function Page() {
return <>{STATIC_HEADER}<Body /></>;
}
SVG:減少座標精度
d="M10.123456,20.654321" → d="M10.12,20.65"。每個數字都會增加位元組;視覺差異小於像素。
透過內聯腳本實現水合無閃爍
對於在水合前就需要的值 (主題、語言環境),請在 React 掛載前,內聯一個 <script> 來設定 document.documentElement.dataset.*。
嚴格地抑制預期的水合不匹配
<time suppressHydrationWarning>{new Date().toLocaleString()}</time>
僅對已知會產生差異的葉節點使用 — 永遠不要用於包含其他子元素的樹。
使用 <Activity> 來控制顯示/隱藏,而不是掛載/卸載
React 19 的 <Activity mode="visible|hidden"> 會保持樹的狀態和 effect 掛載但將其隱藏 — 對於分頁和手風琴式選單來說,比卸載/重新掛載更節省資源。
在條件渲染中使用三元運算子而不是 &&
// 不正確 — `0` 會渲染為文字節點
{count && <Badge>{count}</Badge>}
// 正確
{count > 0 ? <Badge>{count}</Badge> : null}
為載入狀態使用 useTransition
將 startTransition 與動作配對使用;React 會在計算下一個狀態時,將先前的 UI 顯示為 isPending。
React DOM 資源提示
import { preload, preconnect } from "react-dom";
preload("/api/critical", { as: "fetch" });
preconnect("https://api.example.com")
…
[為翻譯而截斷]
7. JavaScript 效能 (低中)
避免在熱門路徑中使用 delete
// 不正確 — 使物件脫離優化模式
const obj = { a: 1, b: 2, c: 3 };
delete obj.b;
// 正確 — 設定為 undefined 或使用展開運算子
const { b, ...rest } = obj; // 或者 obj.b = undefined
優先使用 Map 而非動態物件 {}
對於頻繁的增刪操作,Map 比物件快得多。物件在頻繁的屬性添加/刪除下會脫離 V8 的優化模式。
在渲染期間快取昂貴的計算
// 正確:僅在依賴變化時重新計算
const sortedItems = useMemo(() => heavySort(items), [items]);
避免在渲染期間建立新的函數物件
// 不正確 — 每次渲染都建立新的函數
<Button onClick={() => handleClick(id)} />
// 正確 — 使用 useCallback 或外部處理器
const handleClick = useCallback(() => doSomething(id), [id]);
<Button onClick={handleClick} />
對於記憶體密集型操作使用 requestAnimationFrame
將昂貴的計算分解到多幀中,以避免阻塞主執行緒。
8. 進階模式 (低)
使用 Effect Events 分離 effect 邏輯
import { useEffectEvent } from "react";
const onConnect = useEffectEvent((id) => {
// 可以存取最新的 props/state,無需將它們作為依賴
notifyConnection(id, theme);
});
useEffect(() => {
const socket = connect(roomId);
socket.on("connected", () => onConnect(roomId));
return () => socket.disconnect();
}, [roomId]);
這避免了將 theme 這樣的變數加入 effect 的依賴陣列,從而在主題變化時防止不必要的重新連線。
使用 useOptimistic 實現樂觀更新
import { useOptimistic } from "react";
function MessageList({ messages, sendMessage }) {
const [optimisticMessages, addOptimisticMessage] = useOptimistic(
messages,
(state, newMessage) => [...state, { text: newMessage, sending: true }]
);
return (
<>
{optimisticMessages.map(msg => (
<div>{msg.text} {msg.sending && "⏳"}</div>
))}
<form action={async (formData) => {
addOptimisticMessage(formData.get("message"));
await sendMessage(formData);
}}>
<input name="message" />
</form>
</>
);
}
use Hook (React 19)
import { use } from "react";
function Comments({ commentsPromise }) {
const comments = use(commentsPromise);
return comments.map(c => <li key={c.id}>{c.text}</li>);
}
use 在沒有 async/await 的情況下讀取 Promise。它可以在 if 條件或迴圈中呼叫,並且可以在 Suspense 邊界內處理載入狀態。
穩定的元件參考
// 正確:在模組層級定義,確保參考穩定性
const EmptyState = () => <p>無資料</p>;
function List({ items }) {
return items.length === 0 ? <EmptyState /> : <ItemsList items={items} />;
}
其他規則與屬性
通用的標記與結構規則
- 不要 對使用者說「根據規則 X...」或提及規則名稱或 ID。直接說明應該做什麼。
- 檔案路徑 應使用 POSIX 風格 (
/),且相對於專案根目錄。 - 不要 添加或變更工作流程或自動化規則。
- 請勿 為給定的程式碼檔案產生新的架構、全域變數、狀態管理或複雜的型別系統。
分類法
category: code_quality— 編寫更乾淨、更易於維護的程式碼category: performance— 應用程式效能和效率category: security— 身份驗證、授權、資料驗證和注入防護category: testing— 測試方法論 (如果您偏好不同的方法論,請覆寫此規則)category: architecture— 架構決策、資料模型設計和狀態管理模式category: dx— 開發者體驗和工具化category: accessibility— 無障礙標準和最佳實踐subcategory: react— 特定於 React 的模式subcategory: nextjs— 特定於 Next.js 的模式