大型 React 应用的性能瓶颈:重构 useEffect、缓存策略、调度优先级

511 阅读4分钟

前言: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 18use() 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 应用的体验与响应性。