如何优化并memoize React中基于useReducer的大型reducer
首先别在memoize reducer上浪费时间:reducer本身是纯函数,哪怕有20+个case分支,单次执行耗时也不到1毫秒,在整个渲染链路里占比不足1%,优化它没有任何实际收益。
你遇到的随游玩时长上升的卡顿,90%以上的原因是单Context全量状态下发导致的级联重渲染:任何一个状态字段更新,所有消费了GameContext的组件不管是否用到这个变更字段,都会触发重渲染。游玩越久状态体积越大、挂载的组件越多,重渲染开销就会指数级上涨,直到卡到无法操作。剩下10%的卡顿通常来自内存泄漏、无效深拷贝、高频更新走React渲染流这几个问题。
1. 零成本砍掉无效基础开销
先做这几步,不用改架构就能看到明显提升:
- 打开React DevTools的Profiler面板录一段操作,定位无意义重渲染的组件。所有消费GameContext的组件外层统一包
React.memo,组件内部不要直接拿全量state,只取自己实际用到的字段。 - 排查reducer逻辑:绝对不能写不管字段有没有变化都返回新对象的逻辑,比如无差别
return {...state}、用JSON.parse(JSON.stringify(state))深拷贝全量状态,这类写法的耗时会随状态体积线性上涨,状态大了之后单次dispatch光拷贝就要几十毫秒。所有case分支必须保证:未变更的字段保留原引用,只有实际修改的字段新建对象。 - 你提到的拆分state和dispatch到两个独立Context是必须做的第一步,改造成本极低:dispatch本身引用永久稳定,把dispatch单独放在一个Context里,所有只触发动作、不依赖实时状态的组件(比如操作按钮、事件触发组件)只订阅dispatch Context,就不会因为状态变更触发重渲染,这一步至少能砍掉30%的无效重渲染。
2. 按业务域拆分大Context,不要做全量状态下发
不要维护一个大而全的GameState,按照游戏业务模块拆成多个独立的小Context,每个Context单独管自己的状态和操作逻辑:
- 比如可以拆成
PlayerStateContext(管玩家等级、货币、属性)、InventoryContext(管背包、道具、装备)、CombatContext(管怪物、战斗数值、关卡进度)、GlobalUIContext(管弹窗、引导、设置)这几个独立Provider,嵌套放在组件树上层。 - 每个小Context对应独立的轻量reducer,case数量直接从20+降到个位数,单Context维护的状态体积也会大幅缩小。某个模块的状态变更,只会触发订阅了这个模块Context的组件重渲染,完全不会影响其他无关组件。
- 这步改完基本能解决80%的随时间卡顿问题,也是React官方推荐的Context最佳实践。
3. 细粒度订阅优化,不用拆Context也能避免无效重渲染
如果你暂时不想重构大Context,也完全不需要放弃useReducer换useState+useCallback,用选择器订阅模式就能实现细粒度取值,比手写一堆useCallback稳定得多:
- 不要直接用
useContext(GameContext)拿全量state,自己封装useGameSelector钩子,入参是选择器函数,比如const gold = useGameSelector(state => state.player.gold),只有当选择器返回的值引用发生变化时,才触发当前组件重渲染。 - 这个钩子直接基于React官方的
use-sync-external-store实现(React 18内置,低版本可安装对应npm包),本身就是为状态细粒度订阅设计的,不会出现状态撕裂问题,比自己写useEffect监听state变化靠谱得多。 - 这里不需要memoize整个reducer,只需要用useCallback包裹传给useGameSelector的选择器函数即可,reducer本身的执行开销完全可以忽略。
4. 高频更新状态脱离React渲染流
点击类游戏里有大量高频更新的状态(比如点击飘字、实时伤害数值、动画坐标),这类状态如果走React Context/State更新,每秒触发几十次重渲染,玩久了必然卡顿:
- 高频状态不要放进全局GameState,单独用普通JS对象+轻量事件订阅做存储,需要读值的组件用ref保存引用,更新的时候直接操作DOM,完全不走React重渲染流程。
- 比如点击飘字效果,点击时直接创建DOM元素插在对应位置,动画结束后直接移除即可,零重渲染开销。
完全没必要。useReducer和useState的底层实现是同一套逻辑,性能没有本质差异。你觉得useCallback能实现更细粒度的依赖控制,本质是状态拆分后带来的效果——就算你把所有逻辑换成useState+useCallback,所有值还是塞在一个大Context里全量下发,一样会触发全组件重渲染。
拆分为小Context之后,不管用useReducer还是useState+useCallback,性能差异可以忽略,选你写着顺手的方案即可。
如果做完以上优化还是存在游玩1小时后卡顿的问题,重点排查内存泄漏:
- 检查全局定时器、全局事件监听、动画帧回调有没有在组件卸载/逻辑结束时正确清理,有没有闭包长期引用旧的大状态对象,导致GC无法回收废弃状态树,内存占用随游玩时长持续上涨。
- 可以用Chrome内存工具拍堆快照,检查有没有大量Detached的游离DOM节点、异常留存的旧GameState对象,定位泄漏点修复即可。
- 所有可以通过基础状态计算出来的派生值(比如每秒点击收益、总战斗力)不要存在state里,用useMemo在用到的时候再计算,减少state体积和无效更新。
内容的提问来源于stack exchange,提问作者Maxime T

