性能优化策略
Zustand 的性能优势主要来自:按选择器订阅 + 引用相等短路 + 无 Provider 的 Context 开销。本节从使用侧和原理侧说明如何少渲染、少计算、少持久化开销。
1. 核心机制回顾
| 机制 | 作用 |
|---|---|
选择器 useStore(s => s.x) | 仅当 x 的快照变化时触发该组件重渲染 |
Object.is 比较快照 | 与上一拍 getSnapshot() 相同则跳过渲染 |
外部 store + useSyncExternalStore | 避免整树 Context 广播 |
性能问题几乎都来自:选择器每次返回新引用,或 订阅了过大的状态切片。
2. 细粒度订阅
2.1 只选需要的字段
const bears = useStore((s) => s.bears) // ✅ 仅 bears 变才更新避免 useStore() 无选择器——等价于订阅整个 state,任意字段变化都会重渲染。
2.2 多字段:用 useShallow
选择器若返回新对象/新数组(如 { count, text } 或 [a, b]),每次 getSnapshot 引用都变,会每帧重渲染甚至触发「Maximum update depth」类问题。
做法:对「多字段打包」使用 useShallow,内部对上一拍结果做浅比较,内容不变则返回旧引用,与 Object.is 短路配合。
| 写法 | 结果 |
|---|---|
(s) => ({ a: s.a, b: s.b }) 无 shallow | 几乎每次新对象 → 频繁更新 |
useShallow((s) => ({ a: s.a, b: s.b })) | 仅 a/b 浅层变化时更新 |
原理(一句话):useRef 存上一拍 + shallow 比较;相等则返回缓存引用,不等则更新 ref。
2.3 非 React 环境:subscribeWithSelector
在组件外(模块、路由守卫、图表实例)用 subscribeWithSelector 中间件,对 (state) => slice 订阅,只有 slice 变化才执行 listener,避免全量 subscribe 回调。
3. 中间件与运行时开销
| 中间件 | 优化建议 |
|---|---|
| devtools | 生产环境 enabled: false |
| persist | partialize 只持久化必要字段;大对象注意序列化成本 |
| immer | 适合深层嵌套更新;结构共享减少无效变更,但仍有 proxy 开销 |
| 多层嵌套 | 只组合需要的中间件,顺序影响 set 语义(见 04-中间件) |
4. 应用结构层
4.1 拆分 Store
单一巨大 store 会导致:
- 任意更新使「全量订阅」组件全部刷新;
- 选择器虽可切片,但 DevTools / persist 范围过大。
按领域拆 store(user / cart / ui)或 slices + combine,让订阅面与持久化面自然变小。
4.2 组件层
React.memo子组件 + 父组件只传从 store 选出的原始值或稳定引用。- 避免在
useStore选择器里创建 JSX 或内联对象 props。
5. 批量更新
同一事件处理函数内多次 set:
- Vanilla 层每次
setState会通知 listener; - React 18 自动批处理 多次重渲染为一次(与 React 批处理 一致)。
若需合并为一次通知,可在业务层先算好一次 set(partial),或在中间件里合并(高级用法)。
6. 区分 state 与 actions(结构层)
| 放在 state | 放在 actions(或模块函数) |
|---|---|
| 要触发 UI 更新的数据 | 调 API、读写 localStorage |
| 可被选择器订阅的字段 | 不进入 store 的纯工具函数 |
好处:选择器只订阅 state 时,action 引用变化不会误触发重渲染;partialize 持久化时可只存 state。Setup store 用 ref/state 存数据、普通函数写逻辑即可。
7. 排查清单
- React DevTools Profiler:是否因父级大选择器导致子树全刷?
- 选择器是否返回新对象/数组?→
useShallow或拆订阅。 - 是否误用全量
useStore()? - persist / devtools 是否在生产仍开启?
- v5 升级后无限循环?→ 见 09-常见问题 与官方 Discussion #2790。
8. 总结
| 策略 | 要点 |
|---|---|
| 细粒度订阅 | 选择器只取必要字段 |
| 稳定引用 | 多字段用 useShallow |
| 结构 | 拆 store、memo 子树 |
| 中间件 | 生产关 devtools、persist 部分持久化 |
| 并发 | 依赖 useSyncExternalStore,勿回退到 useState+useEffect 订阅 |
Zustand 本身很轻;性能问题多数是订阅粒度与引用稳定性,而非库内部瓶颈。