用Rematch简化Redux状态管理的实践心得
优化前:卡得不行
我接手这个项目的时候,页面一打开就感觉不对劲。点个按钮要等半秒才有反应,列表滚动像幻灯片,动不动还直接无响应。最离谱的是,某个模块加载完数据后,整个应用卡了将近5秒——用户都以为崩溃了。
这项目用的是 React + Rematch 的组合,状态管理全靠 Rematch。一开始觉得挺清爽,dispatch 几下就更新了,结果越堆越多,model 超过20个,每个都有七八个 reducer 和 effect,state 嵌套三层还深。优化前的首屏加载时间实测是 4.8s,交互延迟普遍在300ms以上,长一点的操作能到1.2s。
用户反馈最多的就是“点了没反应”,其实不是没反应,是 state 更新太慢,组件重渲染太多,浏览器主线程直接被拖死。
找到瓶颈了!
先上 Chrome DevTools 的 Performance 面板跑一遍操作流程。录了一段点击按钮触发数据拉取和更新的过程,结果一看吓一跳:一次 dispatch 后面跟着17次 component render,其中9个是完全没必要更新的。
再看 Components 标签页(React DevTools),发现很多列表项、表单项都在监听全局 state,哪怕只改了个 loading 状态,所有表单控件也跟着 rerender。问题很明显:过度订阅 + 粗粒度的 state 结构。
我还试了用 why-did-you-render 插件,加了之后控制台直接爆红,一堆“你为啥又 render”的警告。有个 SelectInput 组件,在切换 tab 时居然 rerender 了6次,而它根本没用到当前 model 的任何字段。
定位结论就三个:
- state 设计太宽泛,一个 model 存了太多不相关的字段
- 组件 useSelector 过于粗暴,没有做 selector 层级拆分
- reducer 处理逻辑太重,有些甚至在里面做了数组去重、排序这种耗时操作
动刀:拆 model + 精细化 selector
第一刀我砍向 model。原来的 user model 里塞了 profile、permissions、settings、recentLogs,四个本该独立的东西全挤一块儿。每次改 settings,profile 相关组件也收到变更通知。
我把它们拆成四个独立 model:userProfile、userPerm、userSetting、userLog。拆完后每个 model 只负责一块逻辑,state 结构扁平了,订阅关系也清晰了。
然后是 selector 重构。之前很多人图省事直接 useSelector(state => state.user),现在不行了,得精确到字段:
// 优化前
const username = useSelector(state => state.user.profile.name);
// 优化后 —— 抽成独立 selector,还能 memoize
const selectUsername = (state) => state.userProfile.name;
const username = useSelector(selectUsername);
更狠的一招是引入 reselect 做组合计算:
import { createSelector } from 'reselect';
const selectRawList = (state) => state.product.list;
const selectFilter = (state) => state.product.filter;
export const selectFilteredProducts = createSelector(
[selectRawList, selectFilter],
(list, filter) => list.filter(item => item.category === filter.category)
);
这样只有当 list 或 filter 真的变了,才会重新计算结果,避免每次 render 都执行 filter 操作。
Reducer 里别干脏活
我发现有个 effect 请求完数据后,在 reducer 里对返回的数组做了 deepClean + sort + dedupe,三连操作。那个列表有上千条数据,每次进来都卡一下。
这里我改了策略:清洗工作移到 effect 内部异步做完,reducer 只负责最轻量的赋值:
// 优化前
reducers: {
setList: (state, payload) => {
// ❌ 千万别这么干!
const cleaned = payload.map(deepClean).sort(byTime).filter(unique);
return { ...state, list: cleaned };
}
}
// 优化后
effects: (dispatch) => ({
async fetchList() {
const res = await fetch('https://jztheme.com/api/products').then(r => r.json());
// ✅ 在这里处理重操作
const processed = res.map(deepClean).sort(byTime).filter(unique);
dispatch.product.setList(processed); // 此时 reducer 只是简单赋值
}
}),
reducers: {
setList: (state, payload) => ({ ...state, list: payload })
}
这一改,reducer 执行时间从平均 180ms 降到 3ms 以内。别小看这177ms,积少成多,页面流畅感立马不一样。
懒加载 model?Rematch 支持但得手动搞
项目启动时一次性注册所有 model,内存占用高,冷启动慢。我上了 dynamic remount 方案,路由切换时才动态挂载对应 model:
// utils/dynamicModel.js
let mountedModels = new Set();
export function mountModel(store, model) {
if (!mountedModels.has(model.name)) {
store.model(model);
mountedModels.add(model.name);
}
}
export function unmountModel(store, modelName) {
// 注意:Rematch 不原生支持卸载,这里是伪卸载(清数据)
if (mountedModels.has(modelName)) {
store.dispatch({ type: ${modelName}/reset }); // 需提前定义 reset reducer
mountedModels.delete(modelName);
}
}
// page/ProductList.js
useEffect(() => {
mountModel(store, productModel);
return () => unmountModel(store, 'product');
}, []);
配合 code splitting,首屏不需要的 model 根本不加载,首包体积降了18%,冷启动时间从4.8s降到3.1s。
其他小优化(带过)
还有一些零碎但有效的点:
- 给高频更新的 loading 状态单独拆出 uiStatus model,避免影响业务数据订阅
- 用 immer 写 reducer,减少不必要的 immutable 操作失误导致的 rerender
- effect 中连续 dispatch 改成 batchDispatch(自己封装了一个)
优化后:流畅多了
改完上线后,我自己反复点了几轮,明显感觉“跟手”了。同一个操作路径,Performance 面板显示总脚本执行时间从 2100ms 降到 520ms,render 次数从17次降到4次。
实测数据对比:
- 首屏加载:4.8s → 3.1s (-35%)
- 关键交互响应延迟:平均 680ms → 120ms (-82%)
- 内存占用峰值:380MB → 210MB
- 无响应卡顿:从每分钟1.2次降到基本为0
最重要的是用户投诉没了。产品经理过来问是不是换了新框架,我说没有,就是把旧代码理干净了。
性能数据对比
这是正式环境采集的7天平均值(A/B 测试组):
| 指标 | 优化前 | 优化后 | 提升 |
|---|---|---|---|
| 首屏时间 | 4.8s | 3.1s | 35% |
| 交互延迟 | 680ms | 120ms | 82% |
| FPS 平均值 | 41 | 58 | 41% |
虽然还有些边缘 case 会轻微卡顿(比如大数据导出),但主流程已经稳了。
踩坑提醒:这三点一定注意
1. unmountModel 不是真的卸载:Rematch 没提供 removeModel API,所谓“卸载”只能清数据,model 引用还在。小心内存泄漏,尤其是闭包引用。
2. selector 一定要 useMemo 或用 reselect:我一开始写了个 inline selector,结果每次 render 都新建函数,useSelector 失效,白优化。
3. 拆 model 别拆太细:有人一口气拆出50多个 model,维护反而更累。建议按业务域划分,比如订单、用户、权限,别按字段拆。
以上是我的优化经验,有更优的实现方式欢迎评论区交流
这个方案不是理论派那种完美架构,而是边上线边改出来的。有些地方妥协了,比如 batch dispatch 没用 react-redux 的 unstable_batchedUpdates,是因为 Rematch 的 middleware 链接方式冲突。
总之,Rematch 本身不慢,慢的是我们滥用它的方式。状态管理这东西,越早规范,后期越轻松。别等到卡成PPT才想起优化。

暂无评论