停止 React 中的無用重渲染:七條性能止損渲染策略軍規(guī)
我們承諾:無需重寫,讓 React 更順滑。真實(shí)頁面上,p95 提交時(shí)間直接**下降 34%**。
敵人是誰?不是庫(kù),不是框架,而是:被細(xì)碎 props 變化牽一發(fā)動(dòng)全身的雷鳴式重渲染。我們收緊數(shù)據(jù)形態(tài)、固定引用身份,UI 這才安靜下來。
1. 穩(wěn)定 props,堵住子組件的無效重渲染
看起來組件“純”得很,但一旦父組件每次都新建對(duì)象,Diff 只認(rèn)新引用,就會(huì)把子樹走一遍。
我們把 props 做穩(wěn)定與淺層化處理,然后給子組件上 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>;
});于是,與父層無關(guān)的狀態(tài)變化不再牽連子組件,頁面明顯靈動(dòng);滾動(dòng)時(shí) CPU 占用更低,輸入更跟手。
要點(diǎn):優(yōu)先使用“扁平、原始類型”的 props。若必須傳對(duì)象,請(qǐng)?jiān)?/span>邊界處memo,讓子組件跨幀看到相同引用。
2.緩存回調(diào),否則就為 Diff 付費(fèi)
每次渲染都重建 handler,子組件自然要更新。我們用 useCallback 把回調(diào)穩(wěn)住身份。
依賴項(xiàng)盡量瘦身,減少抖動(dòng)。
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} />;
};結(jié)果是:行內(nèi)更新不再漣漪式擴(kuò)散,打字延遲下來了;高頻按鍵不再觸發(fā)布局“驚群”。
要點(diǎn):縮小回調(diào)作用域。如果 handler 依賴太多,拆函數(shù)或用 ref 傳遞穩(wěn)定數(shù)據(jù),把依賴從“多”變“少”。
圖片
3.重列表、重行項(xiàng)要 memo:集合先算,行內(nèi)少算
數(shù)據(jù)漲起來之前,一切“看似正?!?。但當(dāng)量級(jí)上來,昂貴的行與成噸的派生數(shù)據(jù)會(huì)在提交階段鬧翻天。我們給行組件加 memo,并把列表級(jí)派生計(jì)算一次性完成,頁面安靜了。
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} />)}</>);
};滾動(dòng)更穩(wěn)、GC 峰值不再刺眼。更大的數(shù)據(jù)集也能少耗電地扛住。
要點(diǎn):集合層做變換(排序/篩選/聚合),**行層做 memo**。這樣 O(n log n) 的排序成本配上 O(1) 的行更新,體現(xiàn)出線性級(jí)的穩(wěn)定。
4.切片 Context,縮小“爆炸半徑”
一個(gè)肥 Context 一旦變動(dòng),所有消費(fèi)方跟著重渲染。我們把它拆成聚焦切片,更新只觸達(dá)真正需要的組件,整棵樹頓時(shí)安靜。
+--------------------+
| 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購(gòu)物車更新不再重繪用戶菜單與頁眉;在繁忙交互下,p95 路由切換也更加穩(wěn)定。
要點(diǎn):多 Context,單職責(zé)值。若只能保留一個(gè) Provider,就暴露選擇穩(wěn)定切片的自定義 hooks,避免“全員被驚動(dòng)”。
5.拆分狀態(tài),切斷級(jí)聯(lián)重渲染
把多種關(guān)注點(diǎn)塞進(jìn)一個(gè) state 對(duì)象,哪怕是無關(guān)小變動(dòng),也會(huì)波及整塊。我們按更新頻率與作用域拆分,誰變誰渲。
+------------------------+
| state = { a, b, c }    |  (coupled updates)
+------------------------+
           |
           v
+-----+  +-----+  +-----+
| a   |  | b   |  | c   |  (independent updates)
+-----+  +-----+  +-----+高頻計(jì)數(shù)器的微小抖動(dòng),不再頻繁無效化那些慢而貴的派生值;組件樹在突發(fā)壓力下依舊響應(yīng)流暢。
要點(diǎn):用多個(gè) useState/useReducer 桶。把熱而微小與冷而昂貴分開,減少無辜無效化。
6.量化渲染:用數(shù)據(jù)證明優(yōu)化真的有效
我們用一張前/后對(duì)比的小表驗(yàn)證成果:更少的渲染次數(shù) + 更穩(wěn)的耗時(shí),主觀體驗(yàn)自然“變輕”。
+------------+---------+
| Component  | Renders |
+------------+---------+
| Header     | 12 → 3  |
| Row (avg)  |  8 → 2  |
| Footer     |  5 → 1  |
+------------+---------+渲染次數(shù)降下來后,即便在輸入、篩選并發(fā)發(fā)生時(shí),p95 也能保持平穩(wěn)。用戶能明顯感到“卡頓不在”。
要點(diǎn):在開發(fā)環(huán)境用 React DevTools Profiler,或加一個(gè)輕量計(jì)數(shù)器。盯住最熱的少數(shù)組件,并把對(duì)比結(jié)果加入 PR 審查防回退。
7.落地清單:別迷信換庫(kù),先把“身份與邊界”做對(duì)
我們通過固定引用身份、縮小 Context 影響范圍、按真實(shí)邊界拆分狀態(tài),把無效工作擋在了提交階段之外。
結(jié)果?p95 更穩(wěn)定、主線程明顯更平靜。坦白講,我們?cè)?jīng)執(zhí)迷于“換庫(kù)、換框架”,但真兇其實(shí)是數(shù)據(jù)形態(tài)。
記住三句話:
- props 扁平化;
 - **重的就 
memo**; - 合并前先量化。
 
這樣,你的 UI 不用重建也能贏得更多余量。















 
 
 











 
 
 
 