React结合Recoil更新大数组时全组件树重渲染卡顿问题
问题根因
全量重渲染的核心诱因和useSetRecoilState的行为无关,问题出在状态订阅粒度上:你在Page层通过useRecoilState(ListAtom)直接订阅了整个列表原子,只要该原子的顶层引用发生变化(哪怕仅修改数组中某一个项的属性),Page及其所有下游子组件(全部Item)都会被React标记为待重渲染。Recoil不会自动为大数组做项级别的渲染拦截,这是React本身的自上而下渲染机制决定的,不属于API使用错误。
可落地优化方案
1. 拆分原子,实现项级细粒度订阅(最优方案)
放弃用单个Atom存储全量列表的实现,改用两层状态结构把更新范围收敛到单个项:
- 顶层Atom仅存储列表所有项的ID有序数组,不存具体项内容:
const ListIdsAtom = atom({ key: 'listIds', default: [] })
- 用
atomFamily创建按ID索引的项级原子,每个项的状态独立存储:
const ListItemAtom = atomFamily({ key: 'listItem', default: (itemId) => ({ id: itemId, info: '', btnText: '', // 其余项属性 }) })
对应调整组件的订阅逻辑:
Page层仅订阅ListIdsAtom,拿到ID数组后遍历生成Item,将当前项ID作为props传入- 每个
Item组件仅通过useRecoilValue(ListItemAtom(currentItemId))订阅自身对应的项状态,点击按钮更新时也只操作对应ID的ListItemAtom
改造完成后,更新单个项的状态时,仅订阅了该项原子的Item会触发重渲染,其余项因为订阅的状态引用无变化,完全不会进入渲染流程,400项规模下卡顿会完全消失。
2. 暂不改造原子结构的过渡方案:加渲染拦截
如果短期不想调整现有状态结构,可以通过React自带的渲染拦截能力减少无效重渲染:
- 给
Item组件外层套React.memo,做props浅比较跳过无变化的渲染 - 遍历渲染
Item时,所有传入的回调、自定义对象props都用useCallback、useMemo固定引用,避免行内生成临时值导致浅比较失效
注意:该方案只能跳过
Item组件本身的无效渲染,Page和List层依然会在列表更新时触发重渲染,性能收益不如原子拆分方案,但足够解决当前数秒卡顿的问题。
3. 规避无意义的全量副本生成
如果继续使用单Atom存全量列表,更新单一项时生成整个数组的浅拷贝、返回全新状态对象是符合Recoil更新要求的,但这种操作天然会触发所有订阅该原子的组件重渲染,属于大粒度状态设计的固有损耗,没有办法在不调整订阅粒度的前提下完全消除。
内容的提问来源于stack exchange,提问作者PGT
相关产品推荐
相关产品推荐

