停止 React 中的無用重渲染:七條性能止損渲染策略軍規
我們承諾:無需重寫,讓 React 更順滑。真實頁面上,p95 提交時間直接**下降 34%**。
敵人是誰?不是庫,不是框架,而是:被細碎 props 變化牽一發動全身的雷鳴式重渲染。我們收緊數據形態、固定引用身份,UI 這才安靜下來。
1. 穩定 props,堵住子組件的無效重渲染
看起來組件“純”得很,但一旦父組件每次都新建對象,Diff 只認新引用,就會把子樹走一遍。
我們把 props 做穩定與淺層化處理,然后給子組件上 memo。變更落地很快,收益立馬回本。
// Parent.tsx
const Parent = ({ user, onSave }: Props) => {
const view = useMemo(() => ({ theme: "dark" }), []);
return <Profile user={user} view={view} onSave={onSave} />;
};
// Profile.tsx
type P = { user: User; view: { theme: string }; onSave(): void };
export default React.memo(function Profile({ user, view, onSave }: P) {
return <button onClick={onSave}>{user.name} ? {view.theme}</button>;
});于是,與父層無關的狀態變化不再牽連子組件,頁面明顯靈動;滾動時 CPU 占用更低,輸入更跟手。
要點:優先使用“扁平、原始類型”的 props。若必須傳對象,請在邊界處memo,讓子組件跨幀看到相同引用。
2.緩存回調,否則就為 Diff 付費
每次渲染都重建 handler,子組件自然要更新。我們用 useCallback 把回調穩住身份。
依賴項盡量瘦身,減少抖動。
const Editor = ({ value, onCommit }: { value: string; onCommit(v: string): void }) => {
const [draft, setDraft] = useState(value);
const commit = useCallback(() => onCommit(draft), [onCommit, draft]);
const onChange = useCallback((e: React.ChangeEvent<HTMLInputElement>) => {
setDraft(e.target.value);
}, []);
return <input value={draft} onChange={onChange} onBlur={commit} />;
};結果是:行內更新不再漣漪式擴散,打字延遲下來了;高頻按鍵不再觸發布局“驚群”。
要點:縮小回調作用域。如果 handler 依賴太多,拆函數或用 ref 傳遞穩定數據,把依賴從“多”變“少”。
圖片
3.重列表、重行項要 memo:集合先算,行內少算
數據漲起來之前,一切“看似正常”。但當量級上來,昂貴的行與成噸的派生數據會在提交階段鬧翻天。我們給行組件加 memo,并把列表級派生計算一次性完成,頁面安靜了。
const Row = React.memo(function Row({ item }: { item: Item }) {
const price = useMemo(() => item.qty * item.unit, [item.qty, item.unit]);
return <div>{item.name} — {price.toFixed(2)}</div>;
});
const List = ({ items }: { items: Item[] }) => {
const sorted = useMemo(() => [...items].sort((a, b) => a.name.localeCompare(b.name)), [items]);
return (<>{sorted.map(i => <Row key={i.id} item={i} />)}</>);
};滾動更穩、GC 峰值不再刺眼。更大的數據集也能少耗電地扛住。
要點:集合層做變換(排序/篩選/聚合),**行層做 memo**。這樣 O(n log n) 的排序成本配上 O(1) 的行更新,體現出線性級的穩定。
4.切片 Context,縮小“爆炸半徑”
一個肥 Context 一旦變動,所有消費方跟著重渲染。我們把它拆成聚焦切片,更新只觸達真正需要的組件,整棵樹頓時安靜。
+--------------------+
| AppContext (bad) | value = {theme,user,cart,...}
+--------------------+
|
re-render all consumers
v
+------+ +-------+ +------+
|Theme | | User | | Cart |
|Ctx | | Ctx | | Ctx |
+------+ +-------+ +------+
| | |
only theme only user only cart
consumers consumers consumers購物車更新不再重繪用戶菜單與頁眉;在繁忙交互下,p95 路由切換也更加穩定。
要點:多 Context,單職責值。若只能保留一個 Provider,就暴露選擇穩定切片的自定義 hooks,避免“全員被驚動”。
5.拆分狀態,切斷級聯重渲染
把多種關注點塞進一個 state 對象,哪怕是無關小變動,也會波及整塊。我們按更新頻率與作用域拆分,誰變誰渲。
+------------------------+
| state = { a, b, c } | (coupled updates)
+------------------------+
|
v
+-----+ +-----+ +-----+
| a | | b | | c | (independent updates)
+-----+ +-----+ +-----+高頻計數器的微小抖動,不再頻繁無效化那些慢而貴的派生值;組件樹在突發壓力下依舊響應流暢。
要點:用多個 useState/useReducer 桶。把熱而微小與冷而昂貴分開,減少無辜無效化。
6.量化渲染:用數據證明優化真的有效
我們用一張前/后對比的小表驗證成果:更少的渲染次數 + 更穩的耗時,主觀體驗自然“變輕”。
+------------+---------+
| Component | Renders |
+------------+---------+
| Header | 12 → 3 |
| Row (avg) | 8 → 2 |
| Footer | 5 → 1 |
+------------+---------+渲染次數降下來后,即便在輸入、篩選并發發生時,p95 也能保持平穩。用戶能明顯感到“卡頓不在”。
要點:在開發環境用 React DevTools Profiler,或加一個輕量計數器。盯住最熱的少數組件,并把對比結果加入 PR 審查防回退。
7.落地清單:別迷信換庫,先把“身份與邊界”做對
我們通過固定引用身份、縮小 Context 影響范圍、按真實邊界拆分狀態,把無效工作擋在了提交階段之外。
結果?p95 更穩定、主線程明顯更平靜。坦白講,我們曾經執迷于“換庫、換框架”,但真兇其實是數據形態。
記住三句話:
- props 扁平化;
- **重的就
memo**; - 合并前先量化。
這樣,你的 UI 不用重建也能贏得更多余量。





























