用Rematch简化Redux状态管理的实践心得

南宫永香 框架 阅读 1,388
赞 26 收藏
二维码
手机扫码查看
反馈

优化前:卡得不行

我接手这个项目的时候,页面一打开就感觉不对劲。点个按钮要等半秒才有反应,列表滚动像幻灯片,动不动还直接无响应。最离谱的是,某个模块加载完数据后,整个应用卡了将近5秒——用户都以为崩溃了。

用Rematch简化Redux状态管理的实践心得

这项目用的是 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才想起优化。

本文章不代表JZTHEME立场,仅为作者个人观点 / 研究心得 / 经验分享,旨在交流探讨,供读者参考。
发表评论

暂无评论