React 组件重复渲染导致卡顿怎么办?优化指南
问题现象
React 应用出现性能问题:
- 输入框输入时页面明显掉帧
- 大数据列表滚动卡顿
- 控制台提示
Maximum update depth exceeded - React DevTools 显示组件频繁重新渲染
常见原因
| 原因 | 说明 | |------|------| | Context 滥用 | 大对象放入 Context,任何字段变化触发所有订阅组件渲染 | | 父组件渲染带动子组件 | 父组件状态变化,所有子组件重新渲染 | | 内联函数/对象作为 props | 每次渲染创建新引用,触发子组件更新 | | 未使用 key 或 key 使用不当 | 列表渲染效率低下 | | 无意义的状态提升 | 状态放在过高层级,导致大范围重渲染 |
排查步骤
步骤1:使用 React DevTools Profiler
# 1. 安装 React DevTools 浏览器插件
# 2. 打开 Profiler 面板
# 3. 点击录制,执行操作
# 4. 查看渲染次数和耗时
# 重点关注:
# - 哪些组件渲染次数最多
# - 每次渲染耗时最长的组件
# - 不必要的重新渲染
步骤2:使用 why-did-you-render
# 安装
npm install @welldone-software/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,
});
}
# 为组件添加标记
class List extends React.PureComponent {
static whyDidYouRender = true
// ...
}
# 或函数组件
const Item = React.memo(() => {
// ...
});
Item.whyDidYouRender = true;
# 控制台会输出重复渲染的原因
步骤3:检查渲染触发源
// 在组件中添加日志
function MyComponent({ data }) {
console.log('MyComponent render:', {
data,
timestamp: Date.now()
});
return <div>{data.name}</div>;
}
// 观察日志输出频率
// 如果数据没变但频繁渲染,说明是引用问题
优化方法
方法1:使用 React.memo 缓存组件
// 默认行为:父组件渲染,子组件也渲染
function Parent() {
const [count, setCount] = useState(0);
return (
<div>
<button onClick={() => setCount(c => c + 1)}>
Count: {count}
</button>
<ExpensiveChild data={someData} /> // 每次都会重新渲染
</div>
);
}
// 使用 React.memo 缓存
const ExpensiveChild = React.memo(function ExpensiveChild({ data }) {
// 只有 data 变化时才重新渲染
return <div>{/* 复杂渲染 */}</div>;
});
// 自定义比较函数
const ExpensiveChild = React.memo(
function ExpensiveChild({ data }) {
return <div>{/* 复杂渲染 */}</div>;
},
(prevProps, nextProps) => {
// 返回 true 表示不重新渲染
return prevProps.data.id === nextProps.data.id;
}
);
方法2:使用 useMemo 缓存计算结果
// 每次渲染都重新计算
function List({ items, filter }) {
const filteredItems = items
.filter(item => item.name.includes(filter))
.sort((a, b) => b.score - a.score); // 大数据量时很耗时
return <ul>{filteredItems.map(...)}</ul>;
}
// 使用 useMemo 缓存
function List({ items, filter }) {
const filteredItems = useMemo(() => {
return items
.filter(item => item.name.includes(filter))
.sort((a, b) => b.score - a.score);
}, [items, filter]); // 只有依赖变化时才重新计算
return <ul>{filteredItems.map(...)}</ul>;
}
方法3:使用 useCallback 缓存函数
// 每次渲染创建新函数,导致子组件重新渲染
function Parent() {
const [count, setCount] = useState(0);
const handleClick = () => { // 每次都是新函数
console.log('clicked');
};
return (
<div>
<button onClick={() => setCount(c => c + 1)}>
Count: {count}
</button>
<Child onClick={handleClick} /> // 每次传入新函数
</div>
);
}
const Child = React.memo(({ onClick }) => {
return <button onClick={onClick}>Click me</button>;
});
// 使用 useCallback 缓存函数
function Parent() {
const [count, setCount] = useState(0);
const handleClick = useCallback(() => {
console.log('clicked');
}, []); // 空依赖,函数引用稳定
return (
<div>
<button onClick={() => setCount(c => c + 1)}>
Count: {count}
</button>
<Child onClick={handleClick} /> // 函数引用不变
</div>
);
}
方法4:Context 优化 - 拆分和原子化
// 错误:大对象放入 Context
const AppContext = createContext({
user: {},
theme: '',
config: {},
tempInput: '' // 频繁变化
});
// 任何字段变化,所有订阅组件都重新渲染
// 方案1:拆分 Context
const UserContext = createContext();
const ThemeContext = createContext();
const ConfigContext = createContext();
// 组件只订阅需要的 Context
function UserAvatar() {
const user = useContext(UserContext); // 只关心 user
return <img src={user.avatar} />;
}
// 方案2:使用原子化状态管理(推荐)
// 使用 Zustand、Jotai 或 Recoil
// Zustand 示例
import { create } from 'zustand';
const useStore = create((set) => ({
user: {},
theme: 'dark',
tempInput: '',
setUser: (user) => set({ user }),
setTheme: (theme) => set({ theme }),
setTempInput: (input) => set({ tempInput }),
}));
// 组件只订阅需要的字段
function UserAvatar() {
const user = useStore(state => state.user); // 只订阅 user
return <img src={user.avatar} />;
}
function InputComponent() {
const [tempInput, setTempInput] = useStore(
state => [state.tempInput, state.setTempInput]
);
// tempInput 变化不会触发 UserAvatar 重新渲染
}
方法5:虚拟列表优化大数据渲染
# 安装 react-window
npm install react-window
// 直接渲染大量数据
function List({ items }) {
return (
<ul>
{items.map(item => (
<li key={item.id}>{/* 复杂内容 */}</li>
))}
</ul>
);
}
// 10000 条数据直接卡死
// 使用虚拟列表
import { FixedSizeList as List } from 'react-window';
function VirtualList({ items }) {
const Row = ({ index, style }) => (
<div style={style}>
{items[index].name}
</div>
);
return (
<List
height={500} // 列表高度
itemCount={items.length}
itemSize={50} // 每项高度
>
{Row}
</List>
);
}
// 只渲染可视区域内的元素
方法6:状态下沉,避免不必要的提升
// 错误:状态放在过高层级
function App() {
const [inputValue, setInputValue] = useState(''); // 只有 Form 需要
return (
<div>
<Header />
<Form
value={inputValue}
onChange={setInputValue}
/>
<Footer />
</div>
);
}
// inputValue 变化导致整个 App 重新渲染
// 状态下沉到实际使用组件
function App() {
return (
<div>
<Header />
<Form /> // 状态在 Form 内部管理
<Footer />
</div>
);
}
function Form() {
const [inputValue, setInputValue] = useState('');
// 只有 Form 重新渲染
return <input value={inputValue} onChange={...} />;
}
验证优化效果
# 1. 使用 React DevTools Profiler 对比
# 优化前:录制一次,保存截图
# 优化后:录制一次,对比渲染次数
# 2. 使用 Chrome Performance 面板
# 查看帧率是否提升
# 3. 控制台输出
# 添加 console.log 观察渲染次数是否减少
# 4. 使用 Lighthouse 跑分
npm install -g lighthouse
lighthouse http://localhost:3000 --output=html
注意事项
-
不要过度优化:
- 简单组件不需要 React.memo
- 计算量小的操作不需要 useMemo
- 优化有成本,只在必要时使用
-
useMemo/useCallback 不是银弹:
- 它们本身也有开销
- 只有当性能问题确实存在时才使用
-
key 的重要性:
javascript// 错误:使用 index 作为 key {items.map((item, index) => ( <Item key={index} data={item} /> ))} // 正确:使用唯一标识 {items.map(item => ( <Item key={item.id} data={item} /> ))} -
避免内联对象/函数:
javascript// 每次渲染都创建新对象 <Child style={{ color: 'red' }} /> // 提取到外部或使用 useMemo const style = useMemo(() => ({ color: 'red' }), []); <Child style={style} />
总结
| 场景 | 优化方案 | |------|---------| | 子组件不必要的渲染 | React.memo | | 复杂计算 | useMemo | | 函数作为 props | useCallback | | Context 导致大范围渲染 | 拆分 Context 或使用原子化状态 | | 大数据列表 | 虚拟列表(react-window) | | 状态变化影响范围过大 | 状态下沉 |
一句话总结:React 性能优化的核心是"减少不必要的渲染",通过缓存、拆分、虚拟化等手段精准控制更新范围。
问题求助
没能解决你的问题?直接问我
如果你遇到任何技术问题无法解决,可以在这里提交求助。我会尽快查看并回复你。
支持作者
如果这篇文章帮到了你,可以支持我
扫码打赏,支持我持续更新原创排障文章。

