React 性能优化
遇到性能问题时,不要一开始就盲目使用 memo、useMemo 或 useCallback。我们要先通过 React DevTools Profiler 找到真正的性能瓶颈,再进行针对性优化。
React 性能问题通常可以归纳为:
- 不必要的重渲染
- 单次渲染成本过高
- DOM 节点过多
- 更新频率过高
- 首屏资源体积过大
本人推荐的教学视频:
- Youtube: React Performance Optimization Patterns Course
- Youtube: The Most Important Design Pattern in React
- b站:怎么实现预加载?用户体验提升之道 | 什么是渐进式ui | 实现prefetch 【白话】
一、减少不必要的重渲染:合理使用记忆化
使用 React.memo 包裹较重的子组件。当父组件重新渲染时,如果子组件的 props 没有发生变化(浅比较),React 可以跳过子组件的重新渲染。
对于复杂的数据,通常我们需要配合实现记忆化:
useMemo:保持数组、对象等引用稳定useCallback:保持函数引用稳定
jsx
const UserList = React.memo(function UserList({ users, onSelect }) {
return users.map(user => (
<div key={user.id} onClick={() => onSelect(user)}>
{user.name}
</div>
));
});
function App({ users }) {
const visibleUsers = useMemo(
() => users.filter(user => user.active),
[users]
);
const handleSelect = useCallback((user) => {
console.log(user);
}, []);
return (
<UserList
users={visibleUsers}
onSelect={handleSelect}
/>
);
}
需要注意的是,记忆化也有额外开销,不应为了"优化"而到处使用。只有在组件确实存在重复渲染成本时才值得使用。
我们也可以直接使用React Complier实现记忆化。
shell
npm install -D babel-plugin-react-compiler@latest
二、合理拆分组件
合理建立状态边界和渲染边界:
- 将
state放到真正使用它的最小组件中 - 将频繁更新的部分拆分出来
- 避免一个顶层组件管理过多状态
- 让不相关的组件避免被连带重新渲染
例如,输入框的状态通常应该放在输入框组件内部,而不是放在整个页面的顶层组件中。
一个文件尽量只管理同一类职责的 state,例如表单数据由表单相关逻辑负责,加载状态由加载逻辑负责,避免不同类型的状态混杂在一起。
三、避免不必要的派生状态
如果一个值可以通过 props 或现有的 state 计算得到,就不应该再单独保存为 state。
这样可以:
- 减少状态数量
- 避免状态不同步
- 降低额外渲染次数
- 保持单一真实数据来源
jsx
function Cart({ items }) {
const total = items.reduce(
(sum, item) => sum + item.price * item.quantity,
0
);
return <div>总价:{total}</div>;
}
如果计算成本较高,可以使用 useMemo:
jsx
const total = useMemo(() => {
return calculateTotal(items);
}, [items]);
四、控制更新频率
1. 防抖 Debounce
防抖会等待一段时间,确认事件不再触发后才执行。
适合:
- 搜索输入
- 实时校验
- 自动保存
- 用户停止操作后再请求接口
js
function debounce(fn, delay = 300) {
let timer = null;
function debounced(...args) {
clearTimeout(timer);
timer = setTimeout(() => {
fn.apply(this, args);
}, delay);
}
debounced.cancel = () => {
clearTimeout(timer);
timer = null;
};
return debounced;
}
2. 节流 Throttle
节流会限制函数在固定时间内最多执行一次。
适合:
- 滚动事件
- 鼠标移动
- 窗口缩放
- 拖拽操作
js
function throttle(fn, interval = 100) {
let lastTime = 0;
let timer = null;
function throttled(...args) {
const now = Date.now();
const remaining = interval - (now - lastTime);
if (remaining <= 0) {
clearTimeout(timer);
timer = null;
lastTime = now;
fn.apply(this, args);
} else if (!timer) {
timer = setTimeout(() => {
lastTime = Date.now();
timer = null;
fn.apply(this, args);
}, remaining);
}
}
return throttled;
}
3. 使用 useTransition
useTransition 不会减少实际计算量,它主要用于降低非紧急更新对用户交互的阻塞。将指定的状态更新标记为非紧急更新,优先保证输入等交互操作的流畅性。
适合:
- 大量搜索结果渲染
- 复杂列表筛选
- 页面局部内容切换
jsx
const [isPending, startTransition] = useTransition();
function handleChange(value) {
setInputValue(value);
startTransition(() => {
setSearchKeyword(value);
});
}
五、优化大量数据渲染:减少 DOM 数量
当页面需要渲染大量数据时,可以采用以下方式:
保证列表 key 稳定
渲染列表时,应使用数据中稳定且唯一的标识作为 key,例如id。
分页
只渲染当前页数据,减少一次性创建的 DOM 节点。
预加载
在用户即将进入下一页或接近列表底部时,提前请求数据,减少等待时间。
渐进式加载
先渲染核心内容,再逐步加载其他数据,避免页面一次性渲染过多内容。
虚拟化
虚拟化只渲染视口附近的元素,适合超长列表或表格。核心思想是只渲染当前视口附近的少量元素。
常见方案包括:
react-windowreact-virtualized
六、懒加载与代码分割
使用 lazy 和动态 import 实现组件级代码分割,再通过 Suspense 展示加载状态。
jsx
const UserPage = React.lazy(() => import('./UserPage'));
function App() {
return (
<Suspense fallback={<div>加载中...</div>}>
<UserPage />
</Suspense>
);
}
适合懒加载的内容包括:
- 设置页面
- 管理后台
- 富文本编辑器
- 图表组件
- 不常访问的功能模块
这样可以避免用户首次进入页面时一次性下载所有代码,降低首屏资源体积。
除了组件懒加载,还可以关注:
- 路由级代码分割
- 图片压缩和懒加载
- 第三方依赖体积
- Tree Shaking
- SSR、流式渲染和缓存策略
七、合理划分状态管理边界
不同类型的状态,应采用不同的管理方式。
| 状态类型 | 推荐方案 | 典型场景 |
|---|---|---|
| 局部临时状态 | useState、useReducer |
输入框、表单内容、弹窗状态 |
| 低频跨层级状态 | Context |
主题、语言、用户信息 |
| 复杂客户端状态 | Zustand 等状态库 | 购物车、当前 AI 模型、编辑器实例 |
| 服务端状态 | 服务端与数据库 | 权限控制、多端同步、海量数据 |
- 客户端状态:弹窗、表单、购物车、编辑器状态
- 服务端状态(通常需要处理缓存、重新请求、错误、加载和数据同步):用户信息、订单、列表数据、缓存、请求状态
使用 Context 时的注意事项
Context 适合跨层级传递且更新频率较低的数据,但不建议将所有状态放入一个大型 Context 中。
否则可能导致:
- Context 更新时造成非预期的重新渲染
- 状态边界不清晰
- 组件之间耦合增加
- 后期维护困难
可以根据业务拆分不同的 Context,例如:
jsx
<ThemeProvider>
<AuthProvider>
<App />
</AuthProvider>
</ThemeProvider>
八、总结
React 性能优化的核心原则是:
- 先通过 Profiler 定位瓶颈,再进行优化
- 使用
memo、useMemo和useCallback减少不必要的重渲染 - 通过合理拆分组件建立状态边界和渲染边界
- 避免保存不必要的派生状态
- 使用防抖、节流和
useTransition控制更新频率 - 通过分页、渐进式加载和虚拟化减少 DOM 数量
- 使用懒加载降低首屏资源体积
- 根据状态的作用范围选择合适的状态管理方案
- 不要过早优化,避免增加不必要的复杂度
日常开发中,首先应遵循合理的 React 设计模式,写出结构清晰、状态边界合理的代码。只有在真实出现性能问题时,再根据 Profiler 的分析结果进行针对性优化。