一、问题背景:当 Context 成为性能瓶颈

我们团队维护的项目是一个基于 React 18 + TypeScript 的 B 端数据看板,同时有一个 Vue 3 的配置后台。随着功能迭代,状态管理开始失控:
- React 侧:全局使用 useContext 管理用户信息、主题、权限、以及一个实时更新的数据流。任何状态更新(哪怕是一个 isLoading 布尔值)都会导致所有 useContext 订阅者重渲染。
- Vue 侧:使用 provide/inject 传递全局配置,配合 reactive 对象。在表单高频输入时(每秒 30+ 次),inject 的响应式依赖链过长,导致组件更新延迟 120ms 以上。

我们在 Chrome Performance 面板中定位到:React 应用在数据推送时,平均帧渲染时间从 8ms 飙升到 22ms,FPS 一度掉到 45 左右;Vue 应用在表格编辑时,组件更新函数调用次数超过 400 次/秒

二、环境与版本:技术选型基线

迁移前环境基线:

React 18.2.0 + TypeScript 5.1
Vue 3.3.4 + Pinia 2.1.7(原为 provide/inject)
构建工具:Vite 5.0
状态管理库选型:Zustand 4.5.2(React)、Pinia 2.1.7(Vue)

选型理由:
- Zustand:无需 Provider 包裹,支持 useSyncExternalStore,避免 Context 的级联渲染。
- Pinia:官方推荐,支持 Composition API,且 devtools 支持优于自研 provide/inject

三、方案设计:分模块切片与订阅优化

我们决定将原来的全局大 Context 拆分为三个独立的 store

  1. authStore(用户/权限):低频更新,但读取频繁。
  2. themeStore(主题/布局):低频更新。
  3. liveDataStore(实时数据流):高频更新,必须采用选择性订阅

设计原则:
- React 中,使用 useShallow 或手动选择器,确保只有使用 liveData 的组件重渲染。
- Vue 中,Pinia 的 storeToRefs 保证解构后仍具备响应式且按需追踪依赖。

关键点:避免将整个 store 对象传入组件,而是传入基本类型或经 memo 化的切片。

四、核心实现:Zustand 与 Pinia 迁移代码范例

4.1 React 侧:从 Context 到 Zustand

迁移前(Context 典型痛点)

// 旧代码:每次 setState 都会导致所有消费者重渲染
const AppContext = createContext({ liveData: 0, updateData: () => {} });
export const AppProvider = ({ children }) => {
  const [liveData, setLiveData] = useState(0);
  return (

      {children}

  );
};

迁移后(Zustand 选择性订阅)

// 新代码:使用选择器,只有订阅 liveData 的组件才会更新
import { create } from 'zustand';

interface LiveDataStore {
  liveValue: number;
  setLiveValue: (v: number) => void;
}

export const useLiveDataStore = create((set) => ({
  liveValue: 0,
  setLiveValue: (v) => set({ liveValue: v }),
}));

// 组件中:只订阅基本类型,避免对象引用变化
const liveValue = useLiveDataStore((state) => state.liveValue);
const setLiveValue = useLiveDataStore((state) => state.setLiveValue);

// 若需组合多个状态,用 useShallow 防止过度重渲染
import { useShallow } from 'zustand/react/shallow';
const { liveValue, setLiveValue } = useLiveDataStore(
  useShallow((state) => ({ liveValue: state.liveValue, setLiveValue: state.setLiveValue }))
);

数据流变化:当 liveValue 从 0 变为 1 时,只有调用 useLiveDataStore 且选择器依赖 liveValue 的组件重渲染,其他组件(如仅使用 themeStore 的组件)不受影响。

4.2 Vue 侧:从 provide/inject 到 Pinia

迁移前(provide/inject 响应式追踪困难)

// 旧代码:provide 一个大的 reactive 对象,组件 inject 后难以追踪依赖
// 父组件
const globalState = reactive({ user: { name: 'a' }, liveCount: 0, theme: 'dark' });
provide('globalState', globalState);

// 子组件
const state = inject('globalState');
// 任何 state 属性变化,都会触发所有 inject 组件的依赖追踪

迁移后(Pinia 模块化 + storeToRefs)

// 新代码:stores/liveData.ts
import { defineStore } from 'pinia';

export const useLiveDataStore = defineStore('liveData', {
  state: () => ({
    liveCount: 0 as number,
  }),
  actions: {
    increment(step = 1) {
      this.liveCount += step;
    },
  },
});

// 组件中使用:storeToRefs 解构后,只有 liveCount 变化才触发当前组件更新

import { storeToRefs } from 'pinia';
import { useLiveDataStore } from '@/stores/liveData';

const liveStore = useLiveDataStore();
const { liveCount } = storeToRefs(liveStore); // 响应式且按需追踪
const { increment } = liveStore; // action 直接解构



  计数{{ liveCount }}

五、踩坑与优化:迁移过程中的三个关键问题

坑 1:Zustand 的 selector 返回新对象导致死循环
如果你直接 const data = useStore(state => ({ a: state.a, b: state.b })),会导致无限重渲染(因为对象引用每次变化)。解决必须用 useShallow 或返回基本类型。

坑 2:Vue 中直接解构 Pinia store 失去响应性
新手容易 const { liveCount } = useLiveDataStore(),这会丢失响应式。必须使用 storeToRefs

坑 3:跨 store 引用导致循环依赖
在 Zustand 中,如果 authStore 需要读取 liveDataStore 的状态,建议在组件中组合,而非在 store 内部互相引用。我们通过 getState() 静态方法解决,但要注意时序。

性能优化细节
- React 侧,将高频更新的 liveDataStore 与低频的 themeStore 拆开,避免在同一组件中同时订阅。
- Vue 侧,使用 markRaw 忽略不需要响应式的复杂对象(如第三方图表实例)。

六、效果数据:迁移前后性能对比

我们使用 React Profiler 和 Vue Devtools Performance 工具做了 3 轮压测(10 秒内模拟 1000 次数据推送):

指标 迁移前(Context/Provide) 迁移后(Zustand/Pinia) 提升幅度
React 平均渲染帧耗时 22ms 9ms 59% 提升
React FPS(实时数据面板) 45 FPS 58 FPS 28.8% 提升
Vue 组件更新函数调用次数 400次/秒 120次/秒 70% 减少
内存占用(React 侧) 132MB 98MB 25.7% 下降
冗余代码量 基准 -1100 行(拆除了大量 props 透传) -

此外,包体积增加:Zustand 仅 +4.2KB gzip,Pinia 因依赖 vue-demi 增加 +10.8KB gzip,但换来的性能收益完全值得。

七、总结与建议

如果你还在用 Context 管理高频状态,或者用 provide/inject 传递深层响应式数据,我强烈建议你评估迁移到专用状态库。Context 不是状态管理工具,它是依赖注入工具——它只适合低频的、静态的配置(如主题、语言包)。对于 Vue,provide/inject 适合插件开发,不适合业务全局状态。

迁移时最重要的原则:按更新频率拆分 store,并使用选择器/storeToRefs 精准订阅。本次迁移耗时 3 个工作日,其中 40% 时间花在梳理旧代码的 props 透传上。如果你也面临类似问题,不要犹豫,尽早迁移。最后提一句:状态管理库的选择没有银弹,Zustand 和 Pinia 都足够优秀,关键在于你如何用好它们的订阅机制。