This repository has been archived on 2026-05-24. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
all-in-kingsoft/hhs/REACT/2. Hooks 篇/06-性能优化 Hooks.md
T
2026-04-29 22:19:51 +08:00

422 lines
13 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
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<br/>Profiler 中定位?"}
B -->|否| C["先安装 React DevTools<br/>定位瓶颈组件"]
B -->|是| D{"子组件是否因<br/>props 引用变化重渲染?"}
D -->|是| E["给子组件加 React.memo"]
D -->|否| F{"计算是否真的昂贵?"}
F -->|是| G["用 useMemo 缓存结果"]
F -->|否| H["不要优化 — 保持简单"]
E --> I{"回调引用是否<br/>每次都被替换?"}
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<br/>即使 props 内容没变"]
D["useCallback 包装"] --> E["保持同一函数引用"]
E --> F["memo / React.memo<br/>拦截重渲染"]
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<HTMLInputElement>) => {
setQuery(e.target.value);
}, []); // setQuery 引用稳定(setState),可不列入依赖
return <SearchInput value={query} onChange={handleChange} />;
}
// 子组件:配合 React.memo 生效
const SearchInput = React.memo(({ value, onChange }: { value: string; onChange: (e: React.ChangeEvent<HTMLInputElement>) => void }) => {
return <input value={value} onChange={onChange} />;
});
```
> [!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 (
<>
<input value={query} onChange={e => handleChange(e.target.value)} />
{isPending && <Spinner />}
<Results list={results} />
</>
);
}
```
### 语义解读
`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 (
<>
<input value={query} onChange={e => setQuery(e.target.value)} />
{/* 快速响应用户输入 */}
<QuickSuggestions keyword={query} />
{/* 耗时操作延迟渲染 */}
<HeavyResultsList keyword={deferredQuery} />
</>
);
}
```
### 内部工作原理
```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: 如果有更高优先级事件<br/>会暂停 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 (
<>
<label id={labelId}>Username</label>
<input id={inputId} aria-labelledby={labelId} />
</>
);
}
```
### 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
<Profiler id="App" onRender={onRenderCallback}>
<App />
</Profiler>
```
> [!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