Redux性能优化5大技巧:用Selector选择器与记忆化解决组件重复渲染
【免费下载链接】reduxA JS library for predictable global state management项目地址: https://gitcode.com/gh_mirrors/re/redux
Redux 是用于可预测全局状态管理的 JS 库,而Redux 性能优化的核心往往在于:如何用Selector 选择器与**记忆化(Memoization)**解决组件重复渲染的问题。本文面向新手,用 5 个可直接落地的技巧,带你避开"每发一个 action 就全页重渲染"的性能陷阱,让 React + Redux 应用保持流畅 ⚡
一、先搞清楚:为什么组件会"多余地"重复渲染?
在 Redux 的单向数据流中,每次dispatch一个 action,订阅了 store 的组件都会重新执行选择器函数。React-Redux 用===引用比较来判断是否需要重渲染——如果选择器每次都返回一个"新对象 / 新数组",哪怕数据内容一模一样,组件也会被强制重渲染。
这是 Redux 应用最常见的性能问题。官方 FAQ 也明确指出:
使用记忆化的选择器函数是重要的性能考量(Use of memoized selector functions is also an important performance consideration)。
📄 延伸阅读:docs/faq/Performance.md
二、技巧1:用 Selector 选择器封装状态读取,只取最小数据
选择器(Selector)是任何接收 Redux state 并返回其中某个值的函数,相当于"对状态的一次查询"。它有三个好处:
- 🎯封装:只有 reducer 和 selector 知道状态结构,改动只改一处
- ♻️复用:多个组件共享同一段取值逻辑
- 📏最小化选择:组件只订阅真正需要的字段,而不是整个 slice
官方风格指南建议选择器统一命名为selectXxx,如selectTodoById、selectVisibleTodos,并与 reducer 一起定义在 slice 文件中 📄 docs/style-guide/style-guide.md
// 直接查找——普通函数即可,无需记忆化 export const selectTodos = state => state.todos记住第一条黄金法则:状态存得越少越好,能推导的值(过滤、求和、排序)就不要存进 state,而是用选择器现场推导。详见 docs/usage/deriving-data-selectors.md
三、技巧2:别让选择器"每次都返回新引用"
这是新手最容易踩的坑 ⚠️ 看下面这个 Todo 列表组件:
function TodoList() { // ❌ filter() 每次都产生新数组引用 → 每次 action 后都重渲染! const completedTodos = useSelector(state => state.todos.filter(todo => todo.completed) ) }filter()、map()、对象展开{...}这类操作每次都生成新引用,导致组件在每个 action 后都重渲染,即使数据根本没变。
React-Redux 在开发模式下还会主动警告你:
Selector unknown returned a different result when called with the same parameters. This can lead to unnecessary rerenders.判断口诀:选择器返回的是"直接查找的原始引用或基础类型(数字、字符串、布尔)"→ 安全;返回"新数组/新对象"→ 必须处理。
四、技巧3:用 createSelector 记忆化昂贵选择器
记忆化是一种缓存:输入没变,就直接返回上次的结果,不重复计算。Redux 生态的标准工具是 Reselect 的createSelector(Redux Toolkit 已内置导出)。
下面这张 Profiler 截图展示了修复前的"灾难现场":只是刷新了通知列表,<UserPage>却因为非记忆化的filter选择器被白白重渲染了:
修复方式——把selectPostsByUser改写为记忆化选择器:
export const selectPostsByUser = createSelector( // 输入选择器:只负责"取原始值" [selectAllPosts, (state, userId) => userId], // 输出函数:负责"转换计算",仅当输入变化时才重跑 (posts, userId) => posts.filter(post => post.user === userId) )原理很简单:createSelector会对比各输入选择器的结果,只要posts和userId都没变,就直接返回上次的缓存数组(引用不变,组件跳过渲染)。
📄 完整案例(含 Reselect 用法细节与常见错误):docs/usage/deriving-data-selectors.md
💡常见错误:输出函数直接返回输入(
todos => todos)等于没做记忆化;另外createSelector默认缓存只保留最近一组输入,多组件用不同参数复用同一选择器时,需要用"选择器工厂"为每个组件创建独立实例。
五、技巧4:状态规范化,让"按 ID 查数据"变成 O(1)
当数据是长数组时,array.find()逐个遍历找某个 ID 会白白消耗 CPU。官方 FAQ 建议把状态存成**规范化(Normalized)**形状:{ ids: [], entities: {} }——ID 作键的查找表 + 一个排序好的 ID 数组。
Redux Toolkit 的createEntityAdapter帮你自动管理这套结构,并提供现成的selectAll/selectById选择器:
const postsAdapter = createEntityAdapter<Post>({ sortComparer: (a, b) => b.date.localeCompare(a.date) }) const { selectAll: selectAllPosts, selectById: selectPostById } = postsAdapter.getSelectors(state => state.posts)好处有三:
- 🔍 按 ID 直接取值,无需遍历
- 📦 每条数据只存一份,省内存
- 🧊 ID 数组只在增删或排序变化时才更新,天然利于减少重渲染
📄 规范化设计思路:docs/usage/structuring-reducers/NormalizingStateShape.md
六、技巧5:列表组件"父只传 ID、子自选数据",只渲染变化项
React 的默认行为是:父组件重渲染时,会递归渲染所有子组件。所以点一下某条帖子的"点赞"按钮,整条帖子列表里的所有子项都会跟着重渲染:
官方推荐的优化套路是:
- 列表父组件只选择ID 数组(
selectPostIds),ID 没变就不重渲染 - 每个列表项组件通过
postId自己调用useSelector(selectPostById)取数据 - 再配合
React.memo包裹子组件,确保 props 不变就不渲染
优化后的 Profiler 截图——只有被点的那一个组件渲染了:
📄 完整步骤(React.memo 选项、shallowEqual 比较等):docs/tutorials/essentials/part-6-performance-normalization.md
七、记忆化"平衡术":不是所有选择器都要缓存
过度优化同样是坑 ❌ 官方给出的判断标准:
| 场景 | 是否记忆化 |
|---|---|
直接取值:state => state.todos | ❌ 不要,普通函数即可 |
| 推导但返回稳定值:求和、计数(返回数字) | 🤔 可选 |
每次返回新引用:map/filter生成新数组 | ✅ 必须 |
给每个字段都写一个选择器、给每个选择器都套一层createSelector,只会增加维护成本,让 Redux 变成"Java getter 地狱"。只对"返回新引用"或"计算昂贵"的选择器做记忆化,这是最实用的平衡点。
八、常见错误速查清单 📋
- ❌ 在
useSelector里直接filter/map/ 展开对象 → 每次 action 后新引用 → 用createSelector记忆化 - ❌ 选择器输出函数直接返回输入 → 记忆化失效
- ❌ 把整个大 slice 一股脑选进组件 → 只选需要的字段
- ❌ 多个组件共用一个带参数的记忆化选择器却传不同参数 → 缓存永远命中不了 → 用选择器工厂
- ❌ 在 reducer 里做深拷贝(deep clone)→ 所有字段引用全变,触发大面积重渲染;不可变更新只需"浅拷贝受影响的路径"
九、总结
| # | 技巧 | 解决的问题 |
|---|---|---|
| 1 | Selector 选择器封装、最小化选择 | 状态结构散落、组件订阅过多数据 |
| 2 | 避免选择器返回新引用 | 每个 action 后组件无脑重渲染 |
| 3 | createSelector记忆化 | 昂贵计算重复执行 + 新引用强制渲染 |
| 4 | 状态规范化(Entity Adapter) | 大数组遍历查找慢、内存冗余 |
| 5 | 父传 ID + 子自选数据 + React.memo | 长列表"一人点赞全员重渲" |
Redux 本身并不慢——状态树只是一个纯 JS 对象,真正拖慢应用的是"不必要的重复渲染"。掌握选择器与记忆化这两件武器,你的 Redux 应用就能在数据频繁更新的场景下依然保持丝滑 ✨
延伸阅读:
- 选择器完整指南:docs/usage/deriving-data-selectors.md
- 性能与规范化教程:docs/tutorials/essentials/part-6-performance-normalization.md
- Redux 性能 FAQ:docs/faq/Performance.md
- store 与 reducer 源码:src/createStore.ts、src/combineReducers.ts
【免费下载链接】reduxA JS library for predictable global state management项目地址: https://gitcode.com/gh_mirrors/re/redux
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考