React面试宝典【Part5:性能优化】
1. 减少不必要的重渲染(最核心)
(1)React.memo 缓存组件
对函数组件做 props 浅比较,不变就不重渲染。
const Child = React.memo(function Child({ data }) {
return <div>{data}</div>;
});
(2)useCallback 缓存函数引用
传给 memo 子组件的函数必须缓存,否则子组件必重渲染。
const handleClick = useCallback(() => {
setCount(c => c + 1);
}, []);
(3)useMemo 缓存复杂计算结果
避免每次渲染都重新计算大量数据。
const expensiveResult = useMemo(() => {
return hugeList.filter(...).map(...);
}, [hugeList]);
(4)不要在 JSX 里直接写对象/函数
下面这种每次渲染都是新引用,会击穿 memo:
// ❌ bad
<Child onClick={() => {}} data={{}} />
// ✅ good
const handleClick = useCallback(() => {}, []);
const data = useMemo(() => ({}), []);
<Child onClick={handleClick} data={data} />
(5)合理拆分组件
把频繁更新的部分和不怎么变的部分拆开,缩小渲染范围。
2. 列表渲染优化
(1)使用稳定 key
不要用 index,尤其是可删除、可排序的列表。
// ❌ bad
list.map((item, index) => <Item key={index} />)
// ✅ good
list.map(item => <Item key={item.id} />)
(2)大数据列表使用虚拟滚动
只渲染视口内的节点,大幅减少 DOM 数量。
库:react-window、react-virtualized、antd-virtual-list
3. 代码加载优化
(1)React.lazy + Suspense 懒加载组件
const BigComponent = React.lazy(() => import('./BigComponent'));
function App() {
return (
<Suspense fallback={<Loading />}>
<BigComponent />
</Suspense>
);
}
(2)路由代码分割
每个路由单独打包,访问时才加载。
(3)Tree shaking
- 使用 ES6
import/export - 避免全量导入大库(如 lodash)
- 工具:Webpack / Rollup 自动剔除无用代码
4. 渲染计算优化
(1)避免在 render 做昂贵计算
不要在组件内部直接做大量循环、过滤、排序。
// ❌ bad
function App() {
const result = hugeArray.filter(...).sort(...) // 每次渲染都跑
}
// ✅ good
const result = useMemo(() => hugeArray.filter(...), [hugeArray])
(2)避免滥用 Context
一个大 Context 里放一堆状态 → 一个变全量重渲染。
优化:
- 拆成多个小 Context
- 配合 useMemo 缓存 value
- 复杂状态用 Zustand/Jotai/Redux 替代
(3)图片懒加载
loading="lazy"- 滚动到可视区域再加载
5. React 18 并发优化
- useTransition:低优先级渲染,不阻塞输入
- useDeferredValue:延迟值更新,避免界面卡顿
- 自动批处理:所有场景 setState 合并,减少渲染次数
6. 调试与定位
- React DevTools Profiler:看哪些组件在“浪费渲染”
- Performance 面板:看长任务、JS 执行耗时
- 原则:先定位瓶颈,再优化,不要盲目提前优化
总结
React 性能优化主要从四块入手:
- 减少重渲染:memo、useCallback、useMemo、拆分组件
- 列表优化:稳定 key + 虚拟滚动
- 加载优化:懒加载、路由分割、tree-shaking
- 渲染优化:避免昂贵计算、合理拆分 Context、并发特性
一些示例
先演示不优化 → 疯狂重渲染,再一步步加上 React.memo + useCallback + useMemo 优化,可以清晰看到前后区别。
1. 【未优化版本】——子组件无脑重渲染
import { useState } from 'react';
// 子组件(没优化)
function Child({ data, onChange }) {
console.log('Child 重渲染了'); // 每次父更新都会打印
return (
<div>
<p>{data.name}</p>
<button onClick={onChange}>点我</button>
</div>
);
}
// 父组件
export default function App() {
const [count, setCount] = useState(0);
console.log('App 重渲染了');
return (
<div>
<h1>计数:{count}</h1>
<button onClick={() => setCount(count + 1)}>+1</button>
{/* 问题点:直接传对象、直接传函数 */}
<Child
data={{ name: '测试' }}
onChange={() => console.log('click')}
/>
</div>
);
}
现象
- 点击父组件 +1
- App 重渲染了
- Child 也一定会重渲染
明明 Child 的 props 逻辑上没变,却每次都跟着渲染。
原因
data={{ name: '测试' }}每次都是新对象onChange={() => {}}每次都是新函数- 引用变了 → Child 认为 props 变了 → 必重渲染
2. 优化版本 1:给子组件包 React.memo
import { useState } from 'react';
import React from 'react';
// 包一层 memo:props 浅比较,不变就不渲染
const Child = React.memo(function Child({ data, onChange }) {
console.log('Child 重渲染了');
return (
<div>
<p>{data.name}</p>
<button onClick={onChange}>点我</button>
</div>
);
});
export default function App() {
const [count, setCount] = useState(0);
console.log('App 重渲染了');
return (
<div>
<h1>计数:{count}</h1>
<button onClick={() => setCount(count + 1)}>+1</button>
<Child
data={{ name: '测试' }}
onChange={() => console.log('click')}
/>
</div>
);
}
现象
依然无效!Child 还是每次都渲染。
因为 memo 是浅比较,而传递的对象/函数每次都是新引用,浅比较直接判为不等。
3. 最终优化版:memo + useCallback + useMemo
import { useState, useCallback, useMemo } from 'react';
import React from 'react';
// 子组件 memo 缓存
const Child = React.memo(function Child({ data, onChange }) {
console.log('Child 重渲染了');
return (
<div>
<p>{data.name}</p>
<button onClick={onChange}>点我</button>
</div>
);
});
export default function App() {
const [count, setCount] = useState(0);
console.log('App 重渲染了');
// 缓存函数
const onChange = useCallback(() => {
console.log('click');
}, []);
// 缓存对象
const data = useMemo(() => ({
name: '测试'
}), []);
return (
<div>
<h1>计数:{count}</h1>
<button onClick={() => setCount(count + 1)}>+1</button>
{/* 现在传的是稳定引用 */}
<Child data={data} onChange={onChange} />
</div>
);
}
最终效果
- 点击父组件 +1
- App 重渲染
- Child 只在首次渲染一次,之后再也不渲染
优化成功!
4. 再加一个【昂贵计算】的 useMemo 演示
import { useState, useMemo } from 'react';
import React from 'react';
export default function App() {
const [count, setCount] = useState(0);
// 模拟昂贵计算
function heavyCompute() {
console.log('执行昂贵计算');
let sum = 0;
for (let i = 0; i < 1000000000; i++) sum += i;
return sum;
}
// 不用 useMemo:每次渲染都执行
// const total = heavyCompute();
// 用 useMemo:依赖不变只执行一次
const total = useMemo(() => heavyCompute(), []);
return (
<div>
<h1>{count}</h1>
<button onClick={() => setCount(count + 1)}>+1</button>
<p>总和:{total}</p>
</div>
);
}
对比
- 不用 useMemo:每次点 +1 都会卡一下
- 用 useMemo:只有第一次执行,后续丝滑
总结
- React.memo:缓存组件,避免 props 不变时重渲染
- useCallback:缓存函数引用,配合 memo 生效
- useMemo:缓存昂贵计算结果 & 缓存对象引用
- 不要在 JSX 里直接写
{}、(),一定会击穿 memo
Context 滥用导致全组件重渲染 + 拆分优化
- Context 滥用:一个 Provider 里放多状态 → 任一状态变,所有消费组件都会被 Context 强制重渲染。
- 拆分 Context:按功能拆成多个 Provider → 只消费对应 Context 的组件才重渲染,无关组件不受 Context 影响。
- 重要边界:拆分不能阻止“父组件 setState 导致子组件函数执行”,只能阻止“Context 跨状态广播渲染”。
1. 错误写法:单 Context 滥用(渲染风暴)
user 和 theme 放同一个 Context value → 改 theme,UserInfo 也会因 Context 强制重渲染。
import { createContext, useContext, useState } from 'react';
const AppContext = createContext(null);
// 只用到 user
const UserInfo = () => {
console.log('🔵 UserInfo 渲染');
const { user } = useContext(AppContext);
return <div>用户:{user.name}</div>;
};
// 只用到 theme
const ThemePanel = () => {
console.log('🟢 ThemePanel 渲染');
const { theme } = useContext(AppContext);
return <div>主题:{theme}</div>;
};
export default function App() {
const [user, setUser] = useState({ name: '张三' });
const [theme, setTheme] = useState('light');
console.log('🟡 App 渲染');
return (
<AppContext.Provider value={{ user, theme }}>
<UserInfo />
<ThemePanel>
</ThemePanel>
<button onClick={() => setTheme('dark')}>切换主题</button>
</AppContext.Provider>
);
}
点击「切换主题」结果
🟡 App 渲染
🔵 UserInfo 渲染 ❌ 完全没必要,被 Context 强行拖下水
🟢 ThemePanel 渲染 ✅
真实原因
value={{ user, theme }}每次都是新引用- 所有消费该 Context 的组件一律被标记更新
- 即使组件只用到其中一个状态,照样重渲染
2. 正确写法:拆分多个 Context
优化点
- 拆为
UserContext+ThemeContext - 改 theme → 只触发
ThemePanel - 改 user → 只触发
UserInfo
import { createContext, useContext, useState } from 'react';
const UserContext = createContext(null);
const ThemeContext = createContext(null);
const UserInfo = () => {
console.log('🔵 UserInfo 渲染');
const user = useContext(UserContext);
return <div>用户:{user.name}</div>;
};
const ThemePanel = () => {
console.log('🟢 ThemePanel 渲染');
const theme = useContext(ThemeContext);
return <div>主题:{theme}</div>;
};
export default function App() {
const [user, setUser] = useState({ name: '张三' });
const [theme, setTheme] = useState('light');
console.log('🟡 App 渲染');
return (
<UserContext.Provider value={user}>
<ThemeContext.Provider value={theme}>
<UserInfo />
<ThemePanel />
<button onClick={() => setTheme('dark')}>切换主题</button>
</ThemeContext.Provider>
</UserContext.Provider>
);
}
点击「切换主题」结果
🟡 App 渲染
🔵 UserInfo 渲染 ⚠️ 函数执行(父渲染导致)
🟢 ThemePanel 渲染 ✅
优化逻辑
- UserInfo 确实执行了函数(打印 log)
这是父组件 App 重渲染的正常行为,和 Context 无关。 - 但 UserInfo 没有因 Context 变化而产生真实更新
不会走 diff、不会更新 DOM、不会执行副作用,开销极低。 - 对比#1版:
- 滥用:UserInfo 被 Context 强制重渲染
- 拆分:UserInfo 只执行函数,不做更新
这就是 Context 拆分的真实优化效果。
3. 最佳实践:拆分 + memo
import { createContext, useContext, useState, memo } from 'react';
const UserContext = createContext(null);
const ThemeContext = createContext(null);
// 包 memo
const UserInfo = memo(() => {
console.log('🔵 UserInfo 渲染');
const user = useContext(UserContext);
return <div>用户:{user.name}</div>;
});
const ThemePanel = memo(() => {
console.log('🟢 ThemePanel 渲染');
const theme = useContext(ThemeContext);
return <div>主题:{theme}</div>;
});
export default function App() {
const [user, setUser] = useState({ name: '张三' });
const [theme, setTheme] = useState('light');
console.log('🟡 App 渲染');
return (
<UserContext.Provider value={user}>
<ThemeContext.Provider value={theme}>
<UserInfo />
<ThemePanel />
<button onClick={() => setTheme('dark')}>切换主题</button>
</ThemeContext.Provider>
</UserContext.Provider>
);
}
点击「切换主题」最终结果
🟡 App 渲染
// 🔵 UserInfo 不再打印 ✅
🟢 ThemePanel 渲染 ✅
4. 所有渲染规则
| 场景 | 子组件函数执行 | 子组件因 Context 强制更新 | 优化级别 |
|---|---|---|---|
| 单 Context 滥用 | 是 | 是(所有消费组件) | ❌ 最差 |
| 拆分 Context | 是(父渲染导致) | 否(仅对应消费) | ✅ 优化 |
| 拆分 + memo | 否 | 否(仅对应消费) | ✅✅ 最佳 |
5. 总结
Context 滥用问题
把多个无关状态放入同一个 Context,导致一个状态更新,所有消费组件被强制重渲染,产生大量无效更新。拆分优化原理
按业务/功能拆分为多个独立 Context,实现状态更新隔离,只有消费对应 Context 的组件才会更新,避免广播式渲染。重要边界
拆分 Context 不能阻止父组件 setState 带来的子组件函数执行,只能消除 Context 自身的广播渲染。
想要彻底阻止子组件执行,必须搭配React.memo。最佳实践
- 小状态、局部状态不用 Context
- 全局状态按功能拆分多个 Context
- 消费组件使用
memo避免父渲染扩散- 复杂全局状态优先使用状态库(Zustand/Jotai)实现细粒度订阅