React中使用Reducer处理大量子组件数据的最优方案
我想了解在处理大量子组件、通过reducer从状态中提取数据时的性能开销。现有三种实现方式,我直觉认为方案2效率最高,但是否正确?编译后差异是否显著?
方案1:调用一次Reducer,传递完整数据
// 调用一次reducer,传递所有数据 const MainComponent = () => { const { data } = useMyReducer() return ( <> <SubComponent id="one" data={data} /> <SubComponent id="two" data={data} /> <SubComponent id="three" data={data} /> <SubComponent id="four" data={data} /> {/* 更多子组件 */} </> ) } const SubComponent = ({id, data}) => { const myData = data[id] return ( <div>{myData}</div> ) }
方案2:调用一次Reducer,仅传递所需数据
// 调用一次reducer,仅传递所需数据 const MainComponent = () => { const { data } = useMyReducer() return ( <> <SubComponent id="one" data={data.one} /> <SubComponent id="two" data={data.two} /> <SubComponent id="three" data={data.three} /> <SubComponent id="four" data={data.four} /> {/* 更多子组件 */} </> ) } const SubComponent = ({id, data}) => { return ( <div>{data}</div> ) }
方案3:在子组件中调用Reducer
// 在子组件中调用reducer const MainComponent = () => { return ( <> <SubComponent id="one" /> <SubComponent id="two" /> <SubComponent id="three" /> <SubComponent id="four" /> {/* 更多子组件 */} </> ) } const SubComponent = ({id}) => { const { data } = useMyReducer() const myData = data[id] return ( <div>{myData}</div> ) }
补充:useMyReducer的实现
import { useContext } from 'react' import { MyContext } from './context' const useMyReducer = () => { const context = useContext(MyContext) if (context === undefined) { throw new Error('useMyReducer must be used within MyContext') } return context } export default useMyReducer
// context.js import { createContext, useReducer } from 'react' export const MyContext = createContext(initialState) export const MyProvider = ({ children }) => { const [state, dispatch] = useReducer(reducer, {}) // ... reducer相关函数 const value = { state /* 加上其他方法等 */ } return <MyContext.Provider value={value}>{children}</MyContext.Provider> }
性能分析结论
你的直觉没错,方案2确实是三种里性能最优的,在大量子组件的场景下,编译后的性能差异会比较明显,下面逐个说清楚:
方案1的问题
每次data对象里任何一个属性更新,所有SubComponent都会因为接收到新的data引用触发重渲染——React是浅比较,只要data引用变了,哪怕子组件用到的data[id]根本没改,也会重新渲染。另外,每个子组件每次渲染都要做data[id]的查找,单个看开销不大,但子组件多了累加起来也不划算。
方案2的优势
只在父组件调用一次useMyReducer,不用反复读上下文。传给子组件的是具体的data.one、data.two这种值,只有当对应属性真的变了,子组件才会重渲染。其他属性更新时,当前子组件完全不受影响,精准控制了重渲染的范围。而且子组件不用再做属性查找,直接用传入的数据,少了一步运行时计算。
方案3的问题
每个子组件都要调用useMyReducer,本质就是多次读React Context。虽然单次读的开销不大,但子组件多了重复调用的累加损耗就很明显。更糟的是,只要Context里的state有任何更新,所有调用了useMyReducer的子组件都会触发重渲染——哪怕该子组件对应的data[id]一点没变。子组件数量多的时候,这种不必要的重渲染会拖垮性能。
编译后的差异
方案1和2的编译差异主要在父组件传参逻辑:方案1传的是整个对象引用,方案2传单个属性。React编译后的更新逻辑会因为传参类型不同,触发不同的重渲染判断逻辑。方案3的编译结果里每个子组件都包含useContext的调用,React在Context更新时需要遍历所有子组件的Context依赖,比前两个方案的遍历范围大很多,运行时的调度开销更高。
内容的提问来源于stack exchange,提问作者user4893295

