--- tags: [React, Hooks, Performance, useMemo, useCallback, Frontend] create time: 2026-04-29 22:05 --- # 性能优化 Hooks ## 概述 当 React 应用出现不必要的重渲染时,开发者最先想到的是 `useMemo` 和 `useCallback`。但它们也有认知成本——**不恰当的使用反而会让代码更慢更乱**。理解何时该用、何时不该用是关键。 > [!question] 思考:为什么大部分 React 应用根本不需要 useMemo? > 现代浏览器处理一个普通 JavaScript 函数只需几微秒,而 `useMemo` 自身就有缓存对比 + 闭包引用的开销。在没有测量(measure)之前就加记忆化,本质上是猜。 ```mermaid flowchart TD A["发现卡顿"] --> B{"是否在 DevTools
Profiler 中定位?"} B -->|否| C["先安装 React DevTools
定位瓶颈组件"] B -->|是| D{"子组件是否因
props 引用变化重渲染?"} D -->|是| E["给子组件加 React.memo"] D -->|否| F{"计算是否真的昂贵?"} F -->|是| G["用 useMemo 缓存结果"] F -->|否| H["不要优化 — 保持简单"] E --> I{"回调引用是否
每次都被替换?"} I -->|是| J["用 useCallback 稳定引用"] I -->|否| K["不要优化"] style C fill:#F5A87D,color:#000 style H fill:#F5A87D,color:#000 style K fill:#F5A87D,color:#000 style G fill:#4FC08D,color:#fff style J fill:#4FC08D,color:#fff ``` 本文聚焦以下 Hooks,按实用场景排序: | Hook | 作用 | React 版本 | |------|------|------------| | `useMemo` | 缓存计算结果 | 16.8+ | | `useCallback` | 缓存函数引用 | 16.8+ | | `useTransition` | 标记低优先级更新 | 18+ | | `useDeferredValue` | 产生延迟值副本 | 18+ | | `useDebugValue` | 自定义 Hook 调试标记 | 16.8+ | | `useId` | 服务端一致的唯一 ID | 18.3+ | > [!note] 前置知识 > 这些 Hooks 建立在 `useState` / `useEffect` 的基础上。如果你还不熟悉核心 Hooks,请先阅读 [[05-核心 Hooks]]。 --- ## useMemo ```tsx const memoizedValue = useMemo(() => computeExpensive(a, b), [a, b]); ``` ### 执行时机 | 时机 | 行为 | |------|------| | 依赖未变 | 返回上次缓存的值(不调用计算函数) | | 任一依赖变化 | 重新执行计算函数并缓存新结果 | | 组件首次渲染 | 执行计算(无缓存可用) | ### 适用场景 ```tsx // ✅ 适用:昂贵计算 + 频繁 re-render const sortedUsers = useMemo(() => { return users.sort((a, b) => a.name.localeCompare(b.name)); }, [users]); // ✅ 适用:复杂对象/数组衍生值 const userStats = useMemo(() => ({ total: users.length, active: users.filter(u => u.active).length, avgAge: users.reduce((sum, u) => sum + u.age, 0) / users.length, }), [users]); // ❌ 不适用:简单运算(开销比 useMemo 本身还大) const double = useMemo(() => count * 2, [count]); ``` > [!tip] 判断标准 > - 如果表达式只包含 `+ - * /` 或简单的 `.filter()` / `.map()` → 不需要 useMemo > - 如果涉及 API 调用、深层遍历、DOM 测量、大量排序 → 考虑 useMemo > - 如果传给了 `React.memo` 包裹的子组件作为 prop → 优先考虑 useMemo ### 常见陷阱 > [!warning] 依赖陷阱:useMemo 的第二个参数决定了一切 > ```tsx > // ❌ 忘记放入依赖 —— 闭包捕获旧值 > const memo = useMemo(() => items.filter(i => i.price > minPrice), [items]); > // minPrice 变了但 memo 没刷新! > > // ✅ 列出所有外部引用 > const memo = useMemo(() => items.filter(i => i.price > minPrice), [items, minPrice]); > ``` --- ## useCallback ```tsx const memoizedCallback = useCallback( (arg1: string, arg2: number) => doSomething(arg1, arg2), [dep1, dep2], ); ``` ### 本质 ```mermaid flowchart LR A["每次渲染创建新函数"] --> B["子组件收到新引用"] B --> C["子组件 re-render
即使 props 内容没变"] D["useCallback 包装"] --> E["保持同一函数引用"] E --> F["memo / React.memo
拦截重渲染"] style C fill:#F5A87D,color:#000 style F fill:#4FC08D,color:#fff ``` ### 典型使用模式 ```tsx // 父组件:稳定 callback 引用传给子组件 function Parent() { const [query, setQuery] = useState(""); // 不包装:每次 render 都是新函数 → Child 永远 re-render // const handleChange = (e) => setQuery(e.target.value); // 包装:引用稳定,Child 在 query 不变时不重渲染 const handleChange = useCallback((e: React.ChangeEvent) => { setQuery(e.target.value); }, []); // setQuery 引用稳定(setState),可不列入依赖 return ; } // 子组件:配合 React.memo 生效 const SearchInput = React.memo(({ value, onChange }: { value: string; onChange: (e: React.ChangeEvent) => void }) => { return ; }); ``` > [!example] useCallback + React.memo 联合效果 > 见 [[05-核心 Hooks]] 中"**三者联合:防止过度重渲染**"章节,有完整示例。 ### 依赖数组设计原则 > [!summary] 不要把所有局部变量都塞进依赖数组 > ```tsx > // ❌ 错误思路:把所有变量都列上 > const handler = useCallback(() => { > doSomething(a, b, c, d, e, f); > }, [a, b, c, d, e, f]); > // 几乎所有变量都会变 → 几乎每次都生成新函数 → 失去意义 > > // ✅ 正确思路:思考"哪些真正影响这个回调的行为" > const handler = useCallback(() => { > fetchData(keyword); // keyword 是唯一外部依赖 > }, [keyword]); > ``` > > **关键经验**:`setState` setter 函数和 `useRef.current` 引用是稳定的,不需要放入依赖数组。 --- ## useTransition(React 18) ```tsx const [isPending, startTransition] = useTransition(); function SearchPage() { const [query, setQuery] = useState(""); const [results, setResults] = useState([]); const handleChange = (q: string) => { setQuery(q); // 同步更新(立即渲染,高优先级) // 标记为 transition:低优先级更新 startTransition(() => { setResults(performSearch(q)); // 延迟渲染,不阻塞 UI }); }; return ( <> handleChange(e.target.value)} /> {isPending && } ); } ``` ### 语义解读 `useTransition` 的核心思想是:**将一批状态更新分为"马上显示"和"稍后显示"**。`startTransition` 内部的 setState 会被降级为后台优先级,React 可以随时中断它去响应用户交互。 ### 使用场景区别 ```mermaid quadrantChart title "更新优先级划分" x-axis "高优先级" --> "低优先级" y-axis "数据驱动" --> "UI 展示" "onClick → setState": [0.8, 0.9] "路由切换": [0.7, 0.7] "search → results": [0.3, 0.8] "scroll animation": [0.2, 0.3] "typing in input": [0.9, 0.5] ``` ### useTransition vs useDeferredValue | 维度 | useTransition | useDeferredValue | |------|---------------|-------------------| | 控制粒度 | 包裹特定 setState | 包裹整个值 | | 语义 | 启动一个低优先级事务 | 产生一个延迟副本 | | 适合场景 | 表单提交后加载详情 | 搜索框前后分屏 | | 使用复杂度 | 需手动选择哪些更新降级 | 一行封装,自动推导 | > [!tip] 如何选择? > - 你想让 UI 看起来流畅(输入立刻响应,结果稍后出来)→ `useDeferredValue` > - 你有一组相关更新想让它们一起降级 → `useTransition` > - 不确定?先用 `useTransition`,它在大多数场景下都能工作 --- ## useDeferredValue(React 18) ```tsx function SearchPage() { const [query, setQuery] = useState(""); const deferredQuery = useDeferredValue(query); // query 即时更新,deferredQuery 延迟更新 return ( <> setQuery(e.target.value)} /> {/* 快速响应用户输入 */} {/* 耗时操作延迟渲染 */} ); } ``` ### 内部工作原理 ```mermaid sequenceDiagram participant U as 用户输入 participant S as useState (query) participant D as useDeferredValue participant R as React Scheduler U->>S: setQuery("abc") S-->>R: 高优先级更新(立即渲染) Note over R: 输入框立刻显示 "abc" R->>D: query 已更新 D->>R: 请求延迟值(可被抢占) Note over R: 如果有更高优先级事件
会暂停 deferred 渲染 R->>R: 空闲时渲染 HeavyResultsList ``` > [!question] 思考:deferred 值延迟多久? > React 没有固定的延迟时间。它的策略是:"如果有更高优先级的工作要处理(比如用户正在打字),就暂缓 deferred 渲染;等浏览器空闲了再补上。"这使得它既能保证流畅性,又能最终呈现结果。 --- ## useDebugValue(自定义 Hook 调试) > [!abstract] 当你编写自定义 Hook 时,如何在 React DevTools 中看到有意义的值? > 这就是 `useDebugValue` 的用武之地 —— 它为自定义 Hook 添加自定义显示标签。 ```tsx function useOnlineStatus() { const [online, setOnline] = useState(navigator.onLine); // 👇 在 DevTools 中显示可读的 "🟢 Online" 或 "🔴 Offline" useDebugValue(online ? "🟢 Online" : "🔴 Offline"); useEffect(() => { const onOnline = () => setOnline(true); const onOffline = () => setOnline(false); window.addEventListener("online", onOnline); window.addEventListener("offline", onOffline); return () => { window.removeEventListener("online", onOnline); window.removeEventListener("offline", onOffline); }; }, []); return online; } ``` ### 延迟格式化(高性能场景) ```tsx function useFetch(url: string) { const [data, setData] = useState(null); // 延迟格式化:只在 DevTools 展开时才执行 format 函数 // 避免在开发环境中对大型数据集造成额外开销 useDebugValue(data, d => d ? `${d.items?.length || 0} items loaded` : "loading..." ); // ... fetch logic return data; } ``` > [!tip] 生产环境安全 > `useDebugValue` 在生产构建中会被自动忽略,不会产生任何运行时开销。放心在自定义 Hook 中使用。 --- ## useId(React 18.3+) > [!important] SSR 兼容的唯一 ID > `useId` 专为 accessibility 属性设计(如 `aria-labelledby`、`htmlFor`)。它保证了服务端和客户端生成的 ID 完全一致,解决了 hydrate mismatch 问题。 ```tsx function FormField() { // 每次渲染生成唯一且稳定的 ID const labelId = useId(); const inputId = useId(); return ( <> ); } ``` ### useId vs Math.random() vs 手动字符串 | 方式 | SSR 一致 | Hydrate 匹配 | 确定性 | |------|----------|-------------|--------| | `useId()` | ✅ | ✅ | ✅ 基于树结构 | | `Math.random()` | ❌ | ❌ | ❌ 每次随机 | | 手动 `"field-1"` | ✅ | ⚠️ 需人工维护 | ⚠️ 易重复 | > [!warning] useId 不适合用于 key > useId 生成的 ID 是稳定的但不一定是唯一的(同一组件多次渲染可能返回相同 ID)。列表的 `key` 仍应使用业务标识符。 --- ## 性能调优 Checklist > [!summary] 优化顺序 > 不要一开始就用 useMemo/useCallback!按以下顺序排查: 1. **减少不必要的 state** — 能派生的从 state 中移除(derived state) 2. **拆分巨型组件** — 子组件独立维护自己的 state 3. **React DevTools Profiler** — 定位哪个组件在多余 re-render 4. **给高频子组件加 React.memo** 5. **最后才考虑 useMemo / useCallback** ```tsx // Step 1: 先移除所有 useMemo/useCallback 做基准测试 // Step 2: 加上 React.memo 包裹频繁更新的子组件 // Step 3: 仅针对仍存在的瓶颈点添加 useMemo/useCallback ``` ### React DevTools Profiler 实操 ```tsx import { Profiler } from "react"; function onRenderCallback( id: string, // "MyComponent" phase: "mount" | "update", actualDuration: number // 本次渲染耗时(ms) ) { console.log(`${id} ${phase}: ${actualDuration.toFixed(2)}ms`); } // 用法:包裹需要监控的 subtree ``` > [!tip] Profiler API vs DevTools 界面 > - **Profiler API**:适合持续记录生产环境中的性能数据 > - **DevTools 界面**:适合开发阶段交互式点击分析哪个组件导致了重渲染 > 两者可以结合使用。 --- ## 何时绝对不要用这些 Hook? > [!question] 思考:哪些场景加了优化反而适得其反? | 场景 | 原因 | |------|------| | 简单算术运算 | `count * 2` 的开销远小于闭包比较 | | 低频渲染组件 | 一年才重渲染一次的东西不需要优化 | | 顶层容器组件 | 容器本身很少子组件,优化收益为零 | | 嵌套过深的依赖 | `useCallback(() => fn(a,b,c,d,e), [a,b,c,d,e])` → 等价于不用 | > [!note] Google 的前端性能研究结论 > 在他们的实际项目度量中,超过 90% 的重渲染问题通过合理的 `React.memo` 就能解决,真正需要 `useMemo` / `useCallback` 的场景不足 5%。 --- ## 关联笔记 - [[05-核心 Hooks]] — useState / useEffect / useContext / useRef 详解 - [[07-自定义 Hooks]] — 如何设计和组合可复用的自定义 Hook