Redux Toolkit单页40+次重渲染 排查修复方案求助
高频重渲染问题排查与优化方案
问题1:useSelector 是否是高频重渲染的核心原因
是,但问题不在useSelector本身,而在使用方式不符合Redux的重渲染触发规则:
- Redux useSelector的默认重渲染逻辑是:每次全局dispatch触发state更新后,都会执行传入的selector函数,对selector的返回值和上一次返回值做
===严格比较,如果引用不一致就触发当前组件重渲染。 - 最初的写法
const {singleCourse, singleAssignment, contentLibrary} = useSelector(state => state)每次执行都会返回一个新解构出来的对象,哪怕state内容完全没变化,新对象和旧对象的引用永远不相等,等于订阅了整个Redux store的所有变化——全局任何地方触发dispatch(比如弹全局提示、更新用户信息等和当前页面无关的操作),都会导致当前页面重渲染。 - 后续改成三个useSelector分别取三个顶层slice对象的写法,依然存在过度订阅的问题:只要对应slice里的任意字段发生变化(哪怕是当前页面根本用不到的字段),都会触发重渲染。
- 观测到「习题数量越多重渲染次数越高」,是因为每次父组件重渲染时,未做memo优化的习题子组件会全部跟着重渲染,习题量级上来之后渲染开销会明显上涨。
- 另外组件渲染阶段写的
xxx || {}、xxx || []逻辑,每次渲染都会生成新的空对象/空数组引用,如果这些值作为props传给子组件,会进一步触发子组件不必要的重渲染。
createSelector 正确用法
不需要找复杂的业务示例,createSelector的核心作用就是做记忆化:只有当订阅的具体字段发生变化时,才会重新计算返回新的结果引用,否则直接返回上一次缓存的结果,从根源上避免无关更新触发重渲染。适配当前业务的写法示例:
// 可以直接写在页面文件里,也可以抽离到对应slice文件中 import { createSelector } from '@reduxjs/toolkit' const selectAssignmentPageData = createSelector( [ // 只订阅当前页面真正用到的最小粒度字段,不要拿整个slice (state: RootState) => state.selectedAssignment.assignment, (state: RootState) => state.selectedAssignment.dueDateExtensions, (state: RootState) => state.selectedAssignment.documents, (state: RootState) => state.selectedCourse.assignments, (state: RootState) => state.selectedCourse.students, (state: RootState) => state.contentLibrary.library ], (assignment, dueDateExtensions, documents, courseAssignments, courseStudents, library) => { // 默认值统一在这里处理,不会每次渲染都生成新引用 return { assignment: assignment ?? {}, dueDateExtensions: dueDateExtensions ?? [], documents: documents ?? [], courseAssignments: courseAssignments ?? [], courseStudents: courseStudents ?? [], library } } ) // 组件中使用 const pageData = useSelector(selectAssignmentPageData) const { assignment, dueDateExtensions, documents, courseAssignments, courseStudents, library } = pageData
这种写法下,只有列的6个字段的引用发生变化时,组件才会重渲染,其他所有无关的state更新都不会触发当前组件重渲染。
问题2:单页面dispatch 6个动作更新3个reducer是否是不良实践
完全不是,这是非常正常的业务实现方式。
- 页面初始化需要拉取多个维度的业务数据,把不同领域的数据存在对应的独立reducer中,本身就是Redux状态拆分的合理设计,不存在问题。
- 现在感受到的重渲染问题,和dispatch的数量、reducer的拆分方式没有关系,纯粹是取数时订阅粒度过粗导致的。
- 额外说明:现在用
batch包裹6个dispatch的写法,实际上不会批处理接口返回后的state更新——batch只能包住同步执行的dispatch,而6个createAsyncThunk的fulfilled动作是在接口异步返回后分别触发的,天然不在同一个同步上下文里。如果用的是React 18版本,所有更新默认会自动批处理,不需要手动写batch;如果是React 17及以下版本,也不需要强行批处理,只要做好useSelector的精确订阅,哪怕6个更新依次触发,也只会在用到的字段变化时触发必要的重渲染。
额外优化建议
- 长列表(比如习题列表)的列表项组件用
React.memo包裹,避免父组件重渲染时所有列表项跟着无意义重渲染;传给列表项的事件回调函数用useCallback包裹,保证引用稳定。 - 初始化useEffect的依赖数组漏了
courseID和assignmentID,建议补上,避免路由参数变化时出现数据拉取异常。 - 如果后续想进一步简化数据请求逻辑,可以替换成Redux Toolkit自带的RTK Query做数据请求,自带缓存、加载状态管理、自动批处理更新,不需要手动写大量createAsyncThunk和reducer处理逻辑。
内容的提问来源于stack exchange,提问作者OnceAgainNoNet
相关产品推荐
相关产品推荐

