React 性能优化
问题
React 应用如何进行性能优化?如何避免不必要的重渲染?
答案
React 性能优化的核心是减少不必要的渲染和优化渲染成本。主要策略包括:
- 组件级优化:
React.memo、PureComponent - 计算优化:
useMemo、useCallback - 状态管理优化:状态下沉、状态分离
- 渲染优化:懒加载、虚拟列表
React 重渲染机制
什么时候会重渲染?
function Parent() {
const [count, setCount] = useState(0);
return (
<div>
<button onClick={() => setCount(c => c + 1)}>
Count: {count}
</button>
{/* Parent 重渲染时,Child 也会重渲染 */}
{/* 即使 Child 没有接收任何 props */}
<Child />
</div>
);
}
function Child() {
console.log('Child rendered'); // 每次都会执行
return <div>Child Component</div>;
}
父组件重渲染时,所有子组件默认都会重渲染,不管 props 是否变化。这是 React 的默认行为,需要显式优化。
React.memo
基本概念
React.memo 是一个高阶组件(Higher-Order Component),用于缓存函数组件的渲染结果。当组件的 props 没有变化时,跳过重新渲染,直接复用上次的渲染结果。
// 函数签名
function memo<P extends object>(
Component: React.FunctionComponent<P>,
arePropsEqual?: (prevProps: P, nextProps: P) => boolean
): React.MemoExoticComponent<React.FunctionComponent<P>>;
| 参数 | 说明 |
|---|---|
Component | 需要缓存的函数组件 |
arePropsEqual | 可选,自定义比较函数,返回 true 表示相等不重渲染 |
基本用法
React.memo 是一个高阶组件,用于缓存组件,只在 props 变化时重渲染:
interface UserCardProps {
name: string;
age: number;
}
// 使用 React.memo 包裹组件
const UserCard = React.memo(function UserCard({ name, age }: UserCardProps) {
console.log('UserCard rendered');
return (
<div>
<h3>{name}</h3>
<p>Age: {age}</p>
</div>
);
});
function Parent() {
const [count, setCount] = useState(0);
return (
<div>
<button onClick={() => setCount(c => c + 1)}>
Count: {count}
</button>
{/* count 变化时,UserCard 不会重渲染 */}
<UserCard name="Alice" age={25} />
</div>
);
}
自定义比较函数
默认情况下,React.memo 对 props 进行浅比较。可以传入自定义比较函数:
interface ListProps {
items: number[];
onSelect: (item: number) => void;
}
const List = React.memo(
function List({ items, onSelect }: ListProps) {
return (
<ul>
{items.map(item => (
<li key={item} onClick={() => onSelect(item)}>
{item}
</li>
))}
</ul>
);
},
// 自定义比较函数
(prevProps, nextProps) => {
// 返回 true 表示相等,不重渲染
// 返回 false 表示不等,需要重渲染
return (
prevProps.items.length === nextProps.items.length &&
prevProps.items.every((item, i) => item === nextProps.items[i])
);
}
);
| 场景 | 是否推荐 | 原因 |
|---|---|---|
| 频繁重渲染的组件 | ✅ | 减少渲染次数 |
| 渲染成本高的组件 | ✅ | 渲染耗时大于比较耗时 |
| 接收稳定 props 的组件 | ✅ | props 变化少 |
| 总是接收新 props 的组件 | ❌ | 比较没有意义 |
| 轻量级组件 | ❌ | 优化收益小于开销 |
工作原理
React 19 引入了 React Compiler,它会在编译时自动分析组件,为需要的组件添加 memoization。这意味着新项目中可能不再需要手动使用 React.memo。
// React 18:手动优化
const UserCard = React.memo(function UserCard({ name }) {
return <div>{name}</div>;
});
// React 19 + Compiler:自动优化
function UserCard({ name }) {
return <div>{name}</div>;
}
// Compiler 会自动分析并添加 memo
useMemo
基本概念
useMemo 是一个 React Hook,用于缓存计算结果。只有当依赖项变化时才重新计算,否则返回缓存的值。
// 函数签名
function useMemo<T>(factory: () => T, deps: DependencyList): T;
| 参数 | 说明 |
|---|---|
factory | 计算函数,返回需要缓存的值 |
deps | 依赖数组,依赖变化时重新执行 factory |
| 返回值 | factory 的执行结果 |
主要用途
1. 缓存耗时计算
function ProductList({ products, filter }: { products: Product[]; filter: string }) {
// ✅ 只在 products 或 filter 变化时重新计算
const filteredProducts = useMemo(
() => products.filter(p => p.category === filter),
[products, filter]
);
// ✅ 缓存复杂计算
const totalPrice = useMemo(
() => filteredProducts.reduce((sum, p) => sum + p.price, 0),
[filteredProducts]
);
return (
<div>
<p>Total: ${totalPrice}</p>
{filteredProducts.map(product => (
<ProductCard key={product.id} product={product} />
))}
</div>
);
}
2. 保持引用稳定(配合 React.memo)
function Parent() {
const [count, setCount] = useState(0);
// ❌ 每次渲染都创建新数组,导致 Child 重渲染
// const items = [1, 2, 3];
// ✅ 保持引用稳定
const items = useMemo(() => [1, 2, 3], []);
// ❌ 每次渲染都创建新对象
// const config = { theme: 'dark', size: 'large' };
// ✅ 保持引用稳定
const config = useMemo(() => ({ theme: 'dark', size: 'large' }), []);
return (
<div>
<button onClick={() => setCount(c => c + 1)}>Count: {count}</button>
<MemoizedChild items={items} config={config} />
</div>
);
}
useCallback
基本概念
useCallback 是一个 React Hook,用于缓存函数引用。它在每次渲染时返回同一个函数实例(除非依赖项变化)。
// 函数签名
function useCallback<T extends Function>(callback: T, deps: DependencyList): T;
| 参数 | 说明 |
|---|---|
callback | 需要缓存的函数 |
deps | 依赖数组,依赖变化时返回新函数 |
| 返回值 | 缓存的函数引用 |
主要用途
1. 配合 React.memo 阻止子组件重渲染
function Parent() {
const [count, setCount] = useState(0);
const [name, setName] = useState('');
// ❌ 每次渲染都创建新函数
// const handleClick = () => console.log('clicked');
// ✅ 缓存函数引用
const handleClick = useCallback(() => {
console.log('clicked', name);
}, [name]); // 只在 name 变化时更新
return (
<div>
<button onClick={() => setCount(c => c + 1)}>Count: {count}</button>
<input value={name} onChange={e => setName(e.target.value)} />
{/* count 变化时,MemoizedButton 不会重渲染 */}
<MemoizedButton onClick={handleClick} />
</div>
);
}
const MemoizedButton = React.memo(function Button({ onClick }: { onClick: () => void }) {
console.log('Button rendered');
return <button onClick={onClick}>Click me</button>;
});
2. 作为其他 Hook 的依赖
function SearchComponent({ query }: { query: string }) {
// ✅ 缓存搜索函数,避免 useEffect 不必要的重新执行
const search = useCallback(() => {
return fetchResults(query);
}, [query]);
useEffect(() => {
search();
}, [search]); // search 只在 query 变化时变化
}
3. 传递给自定义 Hook
function useFetch<T>(fetcher: () => Promise<T>) {
const [data, setData] = useState<T | null>(null);
useEffect(() => {
fetcher().then(setData);
}, [fetcher]); // fetcher 需要稳定引用
return data;
}
function Component({ id }: { id: string }) {
// ✅ 使用 useCallback 确保 fetcher 稳定
const fetcher = useCallback(() => fetchUser(id), [id]);
const user = useFetch(fetcher);
}
何时使用 useCallback
| 场景 | 是否推荐 | 原因 |
|---|---|---|
| 传递给 memo 组件的回调 | ✅ | 避免子组件重渲染 |
| 作为 useEffect 依赖 | ✅ | 避免 effect 无限执行 |
| 传递给未优化的子组件 | ❌ | 子组件反正会重渲染 |
| 组件内部使用的函数 | ❌ | 不影响重渲染 |
同样地,React Compiler 会自动缓存函数引用。新项目中可以省略大部分手动 useCallback。
// React 18:手动优化
const handleClick = useCallback(() => {
doSomething(a, b);
}, [a, b]);
// React 19 + Compiler:直接写
const handleClick = () => {
doSomething(a, b);
};
// Compiler 会自动处理
useCallback 与 useMemo 的关系
// useCallback(fn, deps) 等价于 useMemo(() => fn, deps)
const memoizedCallback = useCallback(
() => doSomething(a, b),
[a, b]
);
// 等价于
const memoizedCallback = useMemo(
() => () => doSomething(a, b),
[a, b]
);
常见优化模式
模式 1:状态下沉
将状态移到真正需要它的组件中:
// ❌ 状态提升过高,导致整个列表重渲染
function ProductList() {
const [selectedId, setSelectedId] = useState<number | null>(null);
return (
<div>
{products.map(product => (
<ProductCard
key={product.id}
product={product}
isSelected={selectedId === product.id}
onSelect={() => setSelectedId(product.id)}
/>
))}
</div>
);
}
// ✅ 状态下沉到子组件
function ProductCard({ product }: { product: Product }) {
const [isSelected, setIsSelected] = useState(false);
return (
<div
className={isSelected ? 'selected' : ''}
onClick={() => setIsSelected(s => !s)}
>
{product.name}
</div>
);
}
模式 2:组件分离
将频繁变化的部分抽离成单独组件:
// ❌ count 变化导致整个组件重渲染
function App() {
const [count, setCount] = useState(0);
return (
<div>
<button onClick={() => setCount(c => c + 1)}>
Count: {count}
</button>
<ExpensiveComponent /> {/* 每次都重渲染 */}
</div>
);
}
// ✅ 将 count 相关逻辑抽离
function Counter() {
const [count, setCount] = useState(0);
return (
<button onClick={() => setCount(c => c + 1)}>
Count: {count}
</button>
);
}
function App() {
return (
<div>
<Counter />
<ExpensiveComponent /> {/* 不受 count 影响 */}
</div>
);
}
模式 3:children 作为 props
利用 children 避免重渲染:
// ❌ position 变化导致 ExpensiveComponent 重渲染
function ScrollContainer() {
const [position, setPosition] = useState({ x: 0, y: 0 });
return (
<div onScroll={e => setPosition(getPosition(e))}>
<p>Position: {position.x}, {position.y}</p>
<ExpensiveComponent />
</div>
);
}
// ✅ 使用 children 传入
function ScrollContainer({ children }: { children: React.ReactNode }) {
const [position, setPosition] = useState({ x: 0, y: 0 });
return (
<div onScroll={e => setPosition(getPosition(e))}>
<p>Position: {position.x}, {position.y}</p>
{children} {/* 不会重渲染 */}
</div>
);
}
function App() {
return (
<ScrollContainer>
<ExpensiveComponent />
</ScrollContainer>
);
}
children 是在父组件(App)中创建的 React 元素。ScrollContainer 重渲染时,children 的引用保持不变,所以不会重渲染。
模式 4:避免内联对象和函数
// ❌ 每次渲染都创建新对象和新函数
function List() {
return (
<div>
{items.map(item => (
<Item
key={item.id}
style={{ color: 'red', fontSize: 14 }} // 新对象
onClick={() => handleClick(item.id)} // 新函数
/>
))}
</div>
);
}
// ✅ 提取到外部
const itemStyle = { color: 'red', fontSize: 14 };
function List() {
const handleItemClick = useCallback((id: number) => {
handleClick(id);
}, []);
return (
<div>
{items.map(item => (
<Item
key={item.id}
style={itemStyle}
onClick={handleItemClick}
itemId={item.id}
/>
))}
</div>
);
}
const Item = React.memo(function Item({ style, onClick, itemId }: ItemProps) {
return <div style={style} onClick={() => onClick(itemId)}>...</div>;
});
性能分析工具
React DevTools Profiler
// 使用 Profiler API 测量渲染性能
import { Profiler, ProfilerOnRenderCallback } from 'react';
const onRenderCallback: ProfilerOnRenderCallback = (
id, // Profiler id
phase, // "mount" | "update"
actualDuration, // 本次渲染耗时
baseDuration, // 不优化时的预估耗时
startTime, // 开始渲染时间
commitTime // 提交时间
) => {
console.log(`${id} ${phase}: ${actualDuration.toFixed(2)}ms`);
};
function App() {
return (
<Profiler id="ProductList" onRender={onRenderCallback}>
<ProductList />
</Profiler>
);
}
使用 why-did-you-render
// 安装并配置 why-did-you-render
import React from 'react';
if (process.env.NODE_ENV === 'development') {
const whyDidYouRender = require('@welldone-software/why-did-you-render');
whyDidYouRender(React, {
trackAllPureComponents: true,
});
}
// 标记需要追踪的组件
function MyComponent() {
// ...
}
MyComponent.whyDidYouRender = true;
常见面试问题
Q1: useMemo 和 useCallback 有什么区别?
答案:
| 特性 | useMemo | useCallback |
|---|---|---|
| 缓存内容 | 计算结果(任意值) | 函数引用 |
| 返回值 | 执行函数的返回值 | 函数本身 |
| 使用场景 | 复杂计算、稳定引用 | 事件处理函数、传递给子组件 |
// useMemo:缓存计算结果
const expensiveValue = useMemo(() => {
return computeExpensiveValue(a, b);
}, [a, b]);
// useCallback:缓存函数
const handleClick = useCallback(() => {
doSomething(a, b);
}, [a, b]);
// 等价关系
useCallback(fn, deps) === useMemo(() => fn, deps)
Q2: 什么时候不应该使用 useMemo/useCallback?
答案:
- 计算很简单时
// ❌ 不需要 useMemo,简单计算比记忆化开销还小
const sum = useMemo(() => a + b, [a, b]);
// ✅ 直接计算
const sum = a + b;
- 依赖项频繁变化时
// ❌ 每次都会重新计算
const value = useMemo(() => compute(data), [data]); // data 每次都变
- 组件未使用 React.memo 时
// ❌ 子组件没有 memo,useCallback 没有意义
<Child onClick={useCallback(() => {}, [])} />
// ✅ 配合 React.memo 使用
const MemoChild = React.memo(Child);
<MemoChild onClick={useCallback(() => {}, [])} />
Q3: React.memo 的浅比较是什么意思?
答案:
浅比较(Shallow Compare)只比较对象的第一层属性:
// 浅比较的实现
function shallowEqual(objA: any, objB: any): boolean {
if (Object.is(objA, objB)) return true;
if (typeof objA !== 'object' || typeof objB !== 'object') return false;
if (objA === null || objB === null) return false;
const keysA = Object.keys(objA);
const keysB = Object.keys(objB);
if (keysA.length !== keysB.length) return false;
for (const key of keysA) {
if (!Object.hasOwn(objB, key) || !Object.is(objA[key], objB[key])) {
return false;
}
}
return true;
}
// 示例
shallowEqual({ a: 1 }, { a: 1 }); // true
shallowEqual({ a: { b: 1 } }, { a: { b: 1 } }); // false(嵌套对象引用不同)
shallowEqual({ a: [1, 2] }, { a: [1, 2] }); // false(数组引用不同)
Q4: 如何优化长列表的渲染性能?
答案:
- 使用 key:确保 key 稳定且唯一
- React.memo:避免列表项不必要的重渲染
- 虚拟列表:只渲染可见项
- 避免内联函数:使用 useCallback 或传递 id
import { FixedSizeList } from 'react-window';
interface ItemData {
items: Product[];
onSelect: (id: number) => void;
}
const Row = React.memo(function Row({
index,
style,
data
}: {
index: number;
style: React.CSSProperties;
data: ItemData;
}) {
const item = data.items[index];
return (
<div style={style} onClick={() => data.onSelect(item.id)}>
{item.name}
</div>
);
});
function VirtualList({ items }: { items: Product[] }) {
const handleSelect = useCallback((id: number) => {
console.log('Selected:', id);
}, []);
const itemData: ItemData = useMemo(
() => ({ items, onSelect: handleSelect }),
[items, handleSelect]
);
return (
<FixedSizeList
height={400}
width={300}
itemCount={items.length}
itemSize={50}
itemData={itemData}
>
{Row}
</FixedSizeList>
);
}
Q5: 状态提升会导致性能问题吗?如何解决?
答案:
状态提升到父组件后,父组件更新会导致所有子组件重渲染。
解决方案:
- React.memo 包裹子组件
const Child = React.memo(function Child({ value }: { value: string }) {
return <div>{value}</div>;
});
- 状态分离:将频繁变化的状态单独管理
// ❌ inputValue 变化导致所有子组件重渲染
function Parent() {
const [inputValue, setInputValue] = useState('');
const [items, setItems] = useState([]);
// ...
}
// ✅ 将 input 逻辑分离
function SearchInput({ onSearch }: { onSearch: (value: string) => void }) {
const [inputValue, setInputValue] = useState('');
// ...
}
- 使用 Context + useMemo
const ItemsContext = React.createContext<Item[]>([]);
const ActionsContext = React.createContext<Actions>({} as Actions);
function Provider({ children }: { children: React.ReactNode }) {
const [items, setItems] = useState<Item[]>([]);
const actions = useMemo(() => ({
addItem: (item: Item) => setItems(prev => [...prev, item]),
removeItem: (id: number) => setItems(prev => prev.filter(i => i.id !== id)),
}), []);
return (
<ItemsContext.Provider value={items}>
<ActionsContext.Provider value={actions}>
{children}
</ActionsContext.Provider>
</ItemsContext.Provider>
);
}
Q6: React 中如何避免不必要的重渲染?(React.memo、useMemo、useCallback 的组合策略)
答案:
避免不必要重渲染的核心思路是切断重渲染的传播链。React.memo、useMemo、useCallback 三者需要配合使用才能发挥最大效果,单独使用往往没有意义。
组合策略总结:
React.memo 是门卫,useMemo/useCallback 是保证通行证不变的手段。没有门卫,通行证再稳定也没用;有门卫但通行证每次都换,门卫也拦不住。
完整组合示例:
interface TodoListProps {
todos: Todo[];
filter: string;
}
function TodoApp() {
const [todos, setTodos] = useState<Todo[]>([]);
const [filter, setFilter] = useState('all');
const [inputValue, setInputValue] = useState('');
// 1. useMemo:缓存派生数据(对象/数组),保持引用稳定
const filteredTodos = useMemo(
() => todos.filter(todo => {
if (filter === 'completed') return todo.done;
if (filter === 'active') return !todo.done;
return true;
}),
[todos, filter]
);
// 2. useCallback:缓存回调函数,保持引用稳定
const handleToggle = useCallback((id: number) => {
setTodos(prev =>
prev.map(todo =>
todo.id === id ? { ...todo, done: !todo.done } : todo
)
);
}, []);
const handleDelete = useCallback((id: number) => {
setTodos(prev => prev.filter(todo => todo.id !== id));
}, []);
return (
<div>
{/* inputValue 变化时,MemoizedTodoList 不会重渲染 */}
<input value={inputValue} onChange={e => setInputValue(e.target.value)} />
<MemoizedTodoList
todos={filteredTodos}
onToggle={handleToggle}
onDelete={handleDelete}
/>
</div>
);
}
// 3. React.memo:包裹子组件,开启浅比较拦截
const MemoizedTodoList = React.memo(function TodoList({
todos,
onToggle,
onDelete,
}: {
todos: Todo[];
onToggle: (id: number) => void;
onDelete: (id: number) => void;
}) {
console.log('TodoList rendered');
return (
<ul>
{todos.map(todo => (
<MemoizedTodoItem
key={todo.id}
todo={todo}
onToggle={onToggle}
onDelete={onDelete}
/>
))}
</ul>
);
});
const MemoizedTodoItem = React.memo(function TodoItem({
todo,
onToggle,
onDelete,
}: {
todo: Todo;
onToggle: (id: number) => void;
onDelete: (id: number) => void;
}) {
return (
<li>
<span onClick={() => onToggle(todo.id)}>{todo.text}</span>
<button onClick={() => onDelete(todo.id)}>删除</button>
</li>
);
});
常见误区:
| 误区 | 说明 |
|---|---|
只用 React.memo 不用 useCallback | 传入的函数每次都是新引用,memo 比较失败,照样重渲染 |
只用 useCallback 不用 React.memo | 子组件没有浅比较拦截,函数引用稳定也没意义 |
给所有组件加 React.memo | 浅比较本身有开销,轻量组件不值得优化 |
useMemo 依赖项写错 | 依赖缺失导致数据过期,依赖过多导致缓存失效 |
React Compiler 会在编译阶段自动分析并插入 memoization,相当于自动帮你写 memo + useMemo + useCallback。新项目如果启用了 Compiler,大部分手动优化可以省略。
Q7: React DevTools Profiler 怎么用?如何定位性能瓶颈?
答案:
React DevTools Profiler 是 React 官方提供的渲染性能分析工具,可以记录组件的渲染次数、渲染耗时和渲染原因,帮助快速定位性能瓶颈。
使用步骤:
- 安装:Chrome/Firefox 安装 React Developer Tools 浏览器插件
- 录制:打开 DevTools → Profiler 面板 → 点击 Record → 执行操作 → 停止录制
- 分析:查看火焰图和排名图
三种视图模式:
| 视图 | 说明 | 适用场景 |
|---|---|---|
| Flamegraph(火焰图) | 以组件树结构展示渲染耗时,颜色越深表示耗时越长 | 定位哪个组件渲染慢 |
| Ranked(排名图) | 按渲染耗时排序,最慢的组件排在最前 | 快速找到最耗时的组件 |
| Timeline(时间轴) | 展示每次提交的时间线分布 | 查看渲染频率是否异常 |
关键指标解读:
// Profiler API 获取的指标
<Profiler id="App" onRender={(
id, // 组件树 id
phase, // "mount"(首次渲染)或 "update"(更新)
actualDuration, // 本次渲染实际耗时(ms)—— 最关键指标
baseDuration, // 不使用 memo 时的预估耗时
startTime, // 开始渲染的时间戳
commitTime // 提交 DOM 的时间戳
) => {
// actualDuration > 16ms 说明可能造成掉帧
if (actualDuration > 16) {
console.warn(`${id} 渲染耗时 ${actualDuration.toFixed(2)}ms,可能掉帧`);
}
}}>
<App />
</Profiler>
在 Profiler 设置中勾选 "Record why each component rendered while profiling",可以在点击某个组件时看到它重渲染的具体原因(props 变化、state 变化、hooks 变化等)。
实战排查流程:
配合 why-did-you-render 精确诊断:
import React from 'react';
if (process.env.NODE_ENV === 'development') {
const whyDidYouRender = require('@welldone-software/why-did-you-render');
whyDidYouRender(React, {
trackAllPureComponents: true,
logOnDifferentValues: true, // 打印变化的 props/state
});
}
function SuspectedComponent({ data }: { data: Item[] }) {
// 组件逻辑...
return <div>{/* ... */}</div>;
}
SuspectedComponent.whyDidYouRender = true;
// 控制台会输出:
// SuspectedComponent: re-rendered because props.data changed
// prev: [1,2,3] next: [1,2,3] (虽然值相同但引用不同)
Q8: Context 性能问题怎么解决?(拆分 Context、useMemo 包裹 value、状态管理库替代)
答案:
Context 的性能问题根源在于:Provider 的 value 变化时,所有消费该 Context 的组件都会重渲染,无法跳过。即使组件只用了 value 中的某个字段,其他字段变化也会触发重渲染。
// ❌ 典型问题:一个大 Context 装所有状态
const AppContext = createContext({
user: null as User | null,
theme: 'light',
locale: 'zh-CN',
notifications: [] as Notification[],
});
function App() {
const [state, setState] = useState(initialState);
// user 变化时,只使用 theme 的组件也会重渲染!
return (
<AppContext.Provider value={state}>
<UserPanel /> {/* 用 user */}
<ThemeSwitch /> {/* 只用 theme,但 user 变也会重渲染 */}
<NotificationBell /> {/* 只用 notifications,同样受影响 */}
</AppContext.Provider>
);
}
方案 1:拆分 Context
将不同关注点的状态拆分到不同 Context,让组件只订阅需要的部分:
// ✅ 按关注点拆分
const UserContext = createContext<UserContextValue | null>(null);
const ThemeContext = createContext<ThemeContextValue | null>(null);
const NotificationContext = createContext<NotificationContextValue | null>(null);
function AppProviders({ children }: { children: React.ReactNode }) {
return (
<UserProvider>
<ThemeProvider>
<NotificationProvider>
{children}
</NotificationProvider>
</ThemeProvider>
</UserProvider>
);
}
// ThemeSwitch 只消费 ThemeContext,user 变化不会影响它
function ThemeSwitch() {
const { theme, toggleTheme } = useTheme();
return <button onClick={toggleTheme}>{theme}</button>;
}
方案 2:分离数据和操作 + useMemo 包裹 value
interface TodoState {
todos: Todo[];
filter: string;
}
type TodoAction =
| { type: 'ADD'; payload: string }
| { type: 'TOGGLE'; payload: number }
| { type: 'SET_FILTER'; payload: string };
// 数据 Context(会频繁变化)
const TodoStateContext = createContext<TodoState | null>(null);
// 操作 Context(引用稳定,不会变化)
const TodoDispatchContext = createContext<React.Dispatch<TodoAction> | null>(null);
function TodoProvider({ children }: { children: React.ReactNode }) {
const [state, dispatch] = useReducer(todoReducer, initialState);
// ✅ state 是 useReducer 返回的,每次都是新引用(正常)
// ✅ dispatch 是稳定的,不需要 useMemo
return (
<TodoStateContext.Provider value={state}>
<TodoDispatchContext.Provider value={dispatch}>
{children}
</TodoDispatchContext.Provider>
</TodoStateContext.Provider>
);
}
// 只需要触发操作的组件,不会因为 state 变化而重渲染
function AddTodoButton() {
const dispatch = useContext(TodoDispatchContext)!;
return (
<button onClick={() => dispatch({ type: 'ADD', payload: 'New Todo' })}>
添加
</button>
);
}
很多人在 Provider 的 value 中直接传内联对象,导致每次父组件渲染都生成新引用:
// ❌ 每次渲染都创建新对象
<ThemeContext.Provider value={{ theme, toggleTheme }}>
// ✅ 用 useMemo 包裹,只在依赖变化时创建新对象
const value = useMemo(() => ({ theme, toggleTheme }), [theme, toggleTheme]);
<ThemeContext.Provider value={value}>
但注意:如果 toggleTheme 是内联函数,还需要用 useCallback 稳定它,否则 useMemo 的依赖每次都变,等于没缓存。
方案 3:使用状态管理库替代
当 Context 层级嵌套过深、拆分过于复杂时,直接用状态管理库是更好的选择:
import { create } from 'zustand';
interface AppStore {
user: User | null;
theme: 'light' | 'dark';
locale: string;
setUser: (user: User | null) => void;
toggleTheme: () => void;
setLocale: (locale: string) => void;
}
const useAppStore = create<AppStore>((set) => ({
user: null,
theme: 'light',
locale: 'zh-CN',
setUser: (user) => set({ user }),
toggleTheme: () => set((s) => ({ theme: s.theme === 'light' ? 'dark' : 'light' })),
setLocale: (locale) => set({ locale }),
}));
// ✅ 通过 selector 精确订阅,user 变化不会导致 ThemeSwitch 重渲染
function ThemeSwitch() {
const theme = useAppStore((s) => s.theme);
const toggleTheme = useAppStore((s) => s.toggleTheme);
return <button onClick={toggleTheme}>{theme}</button>;
}
// ✅ 无需 Provider 包裹,无嵌套地狱
function App() {
return (
<div>
<ThemeSwitch />
<UserPanel />
</div>
);
}
三种方案对比:
| 维度 | 拆分 Context | 分离数据与操作 | 状态管理库 |
|---|---|---|---|
| 复杂度 | 低 | 中 | 低(库的 API 简单) |
| 精确更新 | 按 Context 粒度 | 数据/操作分离 | Selector 精确订阅 |
| 嵌套问题 | 多 Context 嵌套 | 两层嵌套 | 无需 Provider |
| 适用场景 | 2-3 个全局状态 | 复杂表单/列表 | 状态多、组件复杂 |
| 额外依赖 | 无 | 无 | 需要安装库 |
- 2-3 个全局状态(主题、用户、语言)→ 拆分 Context 足够
- 复杂页面内状态(表单、列表筛选)→ 分离数据与操作
- 状态多且组件间共享复杂 → 直接上 Zustand 等状态管理库
Q9: 父组件通过 props 传递复杂对象给子组件,子组件会触发更新吗?为什么?
答案:
会。父组件重渲染时,默认所有子组件都会重渲染,不管 props 有没有变——这是 React 的默认行为,跟 props 是不是复杂对象无关。
但如果子组件用了 React.memo,情况就不同了。React.memo 通过浅比较判断 props 是否变化:
// 父组件每次渲染都会创建新的对象引用
function Parent() {
const [count, setCount] = useState(0);
// ❌ 每次渲染都是新对象 { name: 'Alice', age: 30 }
// 即使值完全相同,引用不同 → 浅比较判定为"变了"
const user = { name: 'Alice', age: 30 };
// ❌ 每次渲染都是新数组
const tags = ['react', 'ts'];
// ❌ 每次渲染都是新函数
const handleClick = () => console.log('click');
return <MemoChild user={user} tags={tags} onClick={handleClick} />;
}
const MemoChild = React.memo(function Child({
user,
tags,
onClick,
}: {
user: { name: string; age: number };
tags: string[];
onClick: () => void;
}) {
console.log('Child rendered'); // 每次都会打印!memo 白加了
return <div>{user.name}</div>;
});
根本原因:JavaScript 中对象是引用类型,{} !== {}、[] !== []。浅比较用 Object.is 比较每个 prop,引用不同就判定为"变了"。
总结:
| 场景 | 子组件会更新吗? |
|---|---|
无 React.memo,props 不变 | 会(默认行为) |
无 React.memo,props 变了 | 会(默认行为) |
有 React.memo,props 是原始类型且值不变 | 不会 ✅ |
有 React.memo,props 是对象/数组/函数,值不变但引用变了 | 会 ❌ |
有 React.memo + useMemo/useCallback 稳定引用 | 不会 ✅ |
Q10: 如何避免子组件的不必要更新?React.memo 能完全解决这个问题吗?
答案:
React.memo 不能完全解决问题。它只是一个浅比较的门卫,如果传入的 props 每次都是新引用(对象、数组、函数),浅比较永远返回 false,memo 形同虚设。
必须三件套配合使用:
function Parent() {
const [count, setCount] = useState(0);
const [name, setName] = useState('Alice');
// 1. useMemo 稳定对象/数组引用
const user = useMemo(() => ({ name, role: 'admin' }), [name]);
const permissions = useMemo(() => ['read', 'write'], []);
// 2. useCallback 稳定函数引用
const handleSave = useCallback(() => {
console.log('save', name);
}, [name]);
return (
<div>
<button onClick={() => setCount(c => c + 1)}>
count: {count}
</button>
{/* count 变化时,MemoChild 不会重渲染 */}
{/* 因为 user / permissions / handleSave 的引用都没变 */}
<MemoChild user={user} permissions={permissions} onSave={handleSave} />
</div>
);
}
// 3. React.memo 包裹子组件,开启浅比较拦截
const MemoChild = React.memo(function Child({
user,
permissions,
onSave,
}: {
user: { name: string; role: string };
permissions: string[];
onSave: () => void;
}) {
return <div>{user.name}</div>;
});
React.memo 失效的常见场景及解法:
| 失效场景 | 原因 | 解法 |
|---|---|---|
传入内联对象 style={{ color: 'red' }} | 每次创建新对象 | useMemo 或提取为常量 |
传入内联函数 onClick={() => {}} | 每次创建新函数 | useCallback |
传入 children(JSX 元素) | JSX 每次创建新的 React Element | 将 children 提升到更上层组件 |
| Context 值变化 | memo 不拦截 Context 更新 | 拆分 Context 或用状态管理库 |
传入展开的 props {...obj} | obj 内有引用类型 | 只传需要的字段 |
除了 memo 三件套之外的其他手段:
- 状态下沉:把频繁变化的状态下移到真正需要它的子组件内部
- 组件拆分:将频繁变化的部分和稳定的部分拆成两个组件
- children 模式:利用
children不随父组件重渲染的特性(因为 children 的 React Element 是在更上层创建的,引用不变) - 状态管理库:Zustand 的 selector 精确订阅,只有用到的字段变化才更新
Q11: 如果 props 发生了变化,但你不想让子组件更新,该怎么处理?
答案:
这是一个反常规的需求——React 的设计原则是 props 变了就该更新。但实际开发中确实有这种场景,比如某个 prop 只用于初始化、或者某些 prop 变化时不需要重新渲染。
方案 1:自定义比较函数(最直接)
React.memo 的第二个参数可以自定义哪些 props 变化才触发更新:
interface ChartProps {
data: number[]; // 数据变了需要更新
theme: string; // 主题变了需要更新
onHover: () => void; // 这个变了不想更新(只是回调,不影响渲染)
debugId: string; // 这个变了也不想更新(纯调试用途)
}
const Chart = React.memo(
function Chart({ data, theme, onHover, debugId }: ChartProps) {
return <canvas />;
},
(prevProps, nextProps) => {
// 返回 true 表示"相等,不更新"
// 只关心 data 和 theme,忽略 onHover 和 debugId
return (
prevProps.data === nextProps.data &&
prevProps.theme === nextProps.theme
);
}
);
方案 2:useRef 存储不需要触发渲染的值
如果某个 prop 只在事件回调中用到(不参与渲染输出),可以用 ref 引用最新值:
interface EditorProps {
content: string; // 参与渲染
onSave: (c: string) => void; // 不参与渲染,只在事件中调用
}
const Editor = React.memo(
function Editor({ content, onSave }: EditorProps) {
// 将 onSave 存入 ref,避免它的变化触发重渲染
const onSaveRef = useRef(onSave);
// 每次渲染时静默更新 ref(不触发重渲染)
useEffect(() => {
onSaveRef.current = onSave;
});
const handleKeyDown = useCallback((e: KeyboardEvent) => {
if (e.key === 's' && (e.ctrlKey || e.metaKey)) {
e.preventDefault();
// 始终调用最新的 onSave,但 ref 变化不会导致组件更新
onSaveRef.current(content);
}
}, [content]);
return <textarea value={content} onKeyDown={handleKeyDown} />;
},
// 只比较 content,忽略 onSave
(prev, next) => prev.content === next.content
);
方案 3:拆分组件,隔离变化
把需要更新和不需要更新的部分拆成两个组件:
// ❌ title 变化导致整个重渲染(包括昂贵的 Canvas 图表)
function Dashboard({ title, data }: { title: string; data: number[] }) {
return (
<div>
<h1>{title}</h1>
<ExpensiveChart data={data} />
</div>
);
}
// ✅ 拆分后,title 变化只影响 Header,不影响 Chart
function Dashboard({ title, data }: { title: string; data: number[] }) {
return (
<div>
<Header title={title} />
<MemoizedChart data={data} />
</div>
);
}
const Header = function Header({ title }: { title: string }) {
return <h1>{title}</h1>;
};
const MemoizedChart = React.memo(function Chart({ data }: { data: number[] }) {
// 昂贵的渲染逻辑
return <canvas />;
});
方案对比:
| 方案 | 适用场景 | 注意事项 |
|---|---|---|
| 自定义比较函数 | 明确知道哪些 props 影响渲染 | 比较函数要维护,新增 prop 容易遗漏 |
| useRef 存储 | prop 只在回调/副作用中使用,不参与 JSX | 不要用 ref 存储影响渲染输出的值 |
| 组件拆分 | 组件职责太多,不同 props 影响不同区域 | 最符合 React 设计理念,优先考虑 |
props 变了不让组件更新,本质上是在对抗 React 的数据流。大部分场景应该优先考虑:能不能不传这个 prop?能不能换一种数据结构?如果确实需要跳过更新,优先用组件拆分,其次用自定义比较函数,useRef 方案要谨慎使用。