跳到主要内容

React 性能优化

问题

React 应用如何进行性能优化?如何避免不必要的重渲染?

答案

React 性能优化的核心是减少不必要的渲染优化渲染成本。主要策略包括:

  1. 组件级优化React.memoPureComponent
  2. 计算优化useMemouseCallback
  3. 状态管理优化:状态下沉、状态分离
  4. 渲染优化:懒加载、虚拟列表

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])
);
}
);
何时使用 React.memo?
场景是否推荐原因
频繁重渲染的组件减少渲染次数
渲染成本高的组件渲染耗时大于比较耗时
接收稳定 props 的组件props 变化少
总是接收新 props 的组件比较没有意义
轻量级组件优化收益小于开销

工作原理

React 19 变化

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 19 变化

同样地,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 不会重渲染?

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 有什么区别?

答案

特性useMemouseCallback
缓存内容计算结果(任意值)函数引用
返回值执行函数的返回值函数本身
使用场景复杂计算、稳定引用事件处理函数、传递给子组件
// 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?

答案

  1. 计算很简单时
// ❌ 不需要 useMemo,简单计算比记忆化开销还小
const sum = useMemo(() => a + b, [a, b]);

// ✅ 直接计算
const sum = a + b;
  1. 依赖项频繁变化时
// ❌ 每次都会重新计算
const value = useMemo(() => compute(data), [data]); // data 每次都变
  1. 组件未使用 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: 如何优化长列表的渲染性能?

答案

  1. 使用 key:确保 key 稳定且唯一
  2. React.memo:避免列表项不必要的重渲染
  3. 虚拟列表:只渲染可见项
  4. 避免内联函数:使用 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: 状态提升会导致性能问题吗?如何解决?

答案

状态提升到父组件后,父组件更新会导致所有子组件重渲染

解决方案

  1. React.memo 包裹子组件
const Child = React.memo(function Child({ value }: { value: string }) {
return <div>{value}</div>;
});
  1. 状态分离:将频繁变化的状态单独管理
// ❌ inputValue 变化导致所有子组件重渲染
function Parent() {
const [inputValue, setInputValue] = useState('');
const [items, setItems] = useState([]);
// ...
}

// ✅ 将 input 逻辑分离
function SearchInput({ onSearch }: { onSearch: (value: string) => void }) {
const [inputValue, setInputValue] = useState('');
// ...
}
  1. 使用 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 19 + React Compiler

React Compiler 会在编译阶段自动分析并插入 memoization,相当于自动帮你写 memo + useMemo + useCallback。新项目如果启用了 Compiler,大部分手动优化可以省略。

Q7: React DevTools Profiler 怎么用?如何定位性能瓶颈?

答案

React DevTools Profiler 是 React 官方提供的渲染性能分析工具,可以记录组件的渲染次数、渲染耗时和渲染原因,帮助快速定位性能瓶颈。

使用步骤

  1. 安装:Chrome/Firefox 安装 React Developer Tools 浏览器插件
  2. 录制:打开 DevTools → Profiler 面板 → 点击 Record → 执行操作 → 停止录制
  3. 分析:查看火焰图和排名图

三种视图模式

视图说明适用场景
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>
开启 "Record why each component rendered"

在 Profiler 设置中勾选 "Record why each component rendered while profiling",可以在点击某个组件时看到它重渲染的具体原因(props 变化、state 变化、hooks 变化等)。

实战排查流程

配合 why-did-you-render 精确诊断

wdyr.ts
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
});
}
SuspectedComponent.tsx
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,让组件只订阅需要的部分:

拆分 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>
);
}
useMemo 包裹 value 的正确用法

很多人在 Provider 的 value 中直接传内联对象,导致每次父组件渲染都生成新引用:

// ❌ 每次渲染都创建新对象
<ThemeContext.Provider value={{ theme, toggleTheme }}>

// ✅ 用 useMemo 包裹,只在依赖变化时创建新对象
const value = useMemo(() => ({ theme, toggleTheme }), [theme, toggleTheme]);
<ThemeContext.Provider value={value}>

但注意:如果 toggleTheme 是内联函数,还需要用 useCallback 稳定它,否则 useMemo 的依赖每次都变,等于没缓存。

方案 3:使用状态管理库替代

当 Context 层级嵌套过深、拆分过于复杂时,直接用状态管理库是更好的选择:

用 Zustand 替代 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 方案要谨慎使用。

相关链接