前言:90% 的性能问题,都不是代码写得不优雅,而是机制没理解透
很多人写 React 写了几年,一看项目性能拉垮就开骂“React 慢”、“虚拟 DOM 垃圾”。但其实问题大多数出在下面几个地方:
useEffect滥用,副作用泛滥,造成“页面闪、重渲染、数据竞态”- 状态提升 + prop drilling,导致组件树频繁 re-render
- 没有缓存层,所有请求一律实时直打 API
- 渲染优先级未控制,所有更新一律同步卡主线程
- 非关键逻辑和关键逻辑放一起执行,block 住时间切片
如果你正维护一个体量中等偏大的 React 项目(页面数 50+,组件 200+,数据请求频繁),本文将通过一整套 性能组合拳,帮你解决根本问题。
Part 1:停止滥用 useEffect,副作用请回到数据流之外
我们从最常见的写法讲起:
useEffect(() => {
fetch('/api/user').then(setUser);
}, []);
这个写法有什么问题?
- ✅ 能跑
- ❌ 首次渲染会闪一下(先渲染 undefined,再更新)
- ❌ 不可预测(外部状态请求放在 render 后才触发)
解决办法并不只是“加 loading”,而是彻底消除这种闪烁式副作用渲染。
推荐策略:提前计算、避免副作用依赖渲染流程
改法一(预请求 + Suspense):
// UserData.ts
export const getUser = preload(() => fetch('/api/user').then(res => res.json()));
// 页面内
const data = use(getUser());
结合 React 18 的 use() API(或 useSWR 的 prefetch),直接在 render 前拿到数据,跳过 useEffect,同步 render。
Part 2:状态颗粒度拆分:Context 是毒药,全局状态别乱用
很多人一上来就建个 UserContext,然后在全项目乱用:
const { user } = useContext(UserContext);
然后:只要用户信息一变,全项目很多组件都 re-render。
解决办法:
- 拆分 context 为最小颗粒:
UserNameContext,UserAvatarContext - 只在必要处用 context,其余用
selector模式订阅变化 - 用
zustand这样的状态库+subscribeWithSelector只更新真正变化的组件
示例(用 zustand):
const useStore = createStore(set => ({
user: { name: '', avatar: '' },
setUser: u => set({ user: u }),
}));
const useUserName = () => useStore(state => state.user.name);
const useUserAvatar = () => useStore(state => state.user.avatar);
这样改完后,同一个页面中修改 user.name,不会引起使用 user.avatar 的组件重渲染。
Part 3:请求缓存与合并:不要让你的请求打得比人还勤快
React 本身是无状态的,每次进入组件就新拉一次请求,很容易造成以下问题:
- 页面快速切换,重复请求同一接口
- 相同参数多组件并发调用,造成 N 次请求打给后端
- 请求完成顺序乱序,出现“数据被后来的覆盖”的竞态问题
解决办法:构建稳定的请求层,封装缓存、合并、过期策略。
用 SWR 或自己封装一个 memoFetch:
const memoFetch = (() => {
const cache = new Map();
return async (url) => {
if (cache.has(url)) return cache.get(url);
const promise = fetch(url).then(res => res.json());
cache.set(url, promise);
return promise;
};
})();
在组件中使用:
useEffect(() => {
memoFetch('/api/user').then(setUser);
}, []);
也可以和 React 18 的 use() 搭配,构建一个同步等待的数据拉取方式。
Part 4:调度优先级控制:React 不是慢,它只是“你让它一口气干太多事”
默认的 React 更新是同步阻塞型的,比如你用 setState 更新 5 个 state,React 会一次性打包全部执行。
如果你有一些不重要的动画、计数器、输入监听,最好拆出低优先级更新。
React 18 引入了 startTransition:
const [results, setResults] = useState([]);
const handleInput = (text) => {
startTransition(() => {
setResults(doExpensiveSearch(text));
});
};
关键点:
- transition 内的更新是低优先级的,不会阻塞用户输入
- 用户交互响应优先,性能体验会更好
如果你还没升级 React 18,那就是时候升级了。
Part 5:渲染阻塞识别与组件分段懒加载策略
一个页面卡顿,大多数时候是某个组件渲染太慢。常见问题:
- 列表渲染 1000 条 DOM
- 图表组件在首次 render 时同步计算复杂图形
- 某些高阶组件包裹了复杂逻辑但无法按需 lazy
解决方式:
- 对图表组件等封装为懒加载组件
- 使用
React.lazy+Suspense做模块切割 - 对于大列表,用
react-window或虚拟滚动解决
实战例子:
const HeavyChart = lazy(() => import('./ChartComponent'));
<Suspense fallback={<div>loading...</div>}>
<HeavyChart data={data} />
</Suspense>
配合路由切割:
const Route = dynamic(() => import('./route/page'), {
loading: () => <Skeleton />,
});
总结:一整套性能优化组合拳
| 技术策略 | 目标 |
|---|---|
| use() + preload | 避免 useEffect 闪烁式副作用数据获取 |
| 状态拆粒 + store | 避免全组件树 re-render |
| 请求缓存 + 去重 | 降低接口压力,避免重复计算 |
| startTransition | 优化输入响应性能,解耦 UI 渲染阻塞 |
| 懒加载 + Suspense | 避免非关键组件阻塞主线程 |
| 虚拟滚动 | 大数据渲染优化,提升滚动和响应性能 |
这些策略组合使用,可以在不更换框架的情况下,大幅提升 React 应用的体验与响应性。