记录一次react渲染优化
一、需要解决的问题
1.首次加载缓慢;
2.修改表单时,有时会修改失败。
二、问题分析
1.使用react-scan插件查看加载慢的问题,发现首次进入页面的时候会页面全屏重复加载多次的问题,每次修改表单内容都会触发整个应用的重新渲染。
2.重复的React.useEffect订阅,有多个useEffect同时订阅store的变化。
3.更新函数触发连锁反应,每当表单项发生变化时, `updateItem` 会更新areas,这触发了areas订阅,由于订阅的store是绑定的全局app.store所以会造成全局渲染。
三、问题解决
1. 优化订阅逻辑,减少不必要的更新触发
// 修改前
React.useEffect(() =>store.subscribe((s) => s.areas,(areas) => {if (!task || step !== 1) returnconsole.log('areas changed')updateTaskThrottledCallback({ ...task, areas })}),[task, step])React.useEffect(() =>store.subscribe((s) => s.roiTransformer,(roiTransformer) => {if (!task || step !== 1) returnconsole.log('roiTransformer changed')updateTaskThrottledCallback({ ...task, roiTransformer })}),[task, step])React.useEffect(() =>store.subscribe((s) => s.imagePreprocessing,(imagePreprocessing) => {if (!task || step !== 1) returnconsole.log('imagePreprocessing changed')updateTaskThrottledCallback({ ...task, imagePreprocessing })}),[task, step])React.useEffect(() => {if (!device) returnsetConfiguringStep(device.id, step)setTaskName(task?.name ?? '')}, [step])// 修改后React.useEffect(() =>store.subscribe((s) => ({ areas: s.areas, roiTransformer: s.roiTransformer, imagePreprocessing: s.imagePreprocessing }),({ areas, roiTransformer, imagePreprocessing }) => {if (!task || step !== 1) return// 添加深度比较,避免因为对象引用变化而触发不必要的更新const hasRealChanges = JSON.stringify(task.areas) !== JSON.stringify(areas) ||JSON.stringify(task.roiTransformer) !== JSON.stringify(roiTransformer) ||JSON.stringify(task.imagePreprocessing) !== JSON.stringify(imagePreprocessing)if (!hasRealChanges) returnconsole.log('task data changed')updateTaskThrottledCallback({ ...task, areas, roiTransformer, imagePreprocessing })},{equalityFn: (a, b) =>a.areas === b.areas &&a.roiTransformer === b.roiTransformer &&a.imagePreprocessing === b.imagePreprocessing}),[task, step])
2. 拆解store
使用一个对应模块的store去拆解全局的app.store或者新建一个临时的store,在合适的时机(如点击完成按钮),去更新到全局的store.
3. 表单组件的优化
使用 useCallback 包裹了 handleItemChecked 、 handleItemChanged 和 handleFormatChange 等更新方法.
useCallback
是一个允许你在多次渲染中缓存函数的 React Hook。useCallback – React 中文文档
memo
允许你的组件在 props 没有改变的情况下跳过重新渲染。memo – React 中文文档
使用Memo包裹子组件
const Item = React.memo(({ item, itemIndex, handleItemChanged, handleFormatChange }) => {// 组件逻辑
})
4. 事件处理函数优化
// 优化前
const handleItemChecked = (itemIndex: number, checked: boolean) => {// 直接更新逻辑
}// 优化后
const handleItemChecked = useCallback((itemIndex: number, checked: boolean) => {const item = tool.config.form.items[itemIndex]if (item.enabled === checked) return // 避免重复更新const updatedItem = { ...item, enabled: checked }tempFormStore.getState().addPendingUpdate(areaIndex, toolIndex, itemIndex, updatedItem)
}, [tool.config.form.items, areaIndex, toolIndex])
5. 订阅逻辑优化
缺乏深度比较,对象引用变化就会触发更新
没有检查数据是否真正发生变化
// 优化前
React.useEffect(() =>store.subscribe((s) => ({ areas: s.areas, roiTransformer: s.roiTransformer, imagePreprocessing: s.imagePreprocessing }),({ areas, roiTransformer, imagePreprocessing }) => {if (!task || step !== 1) returnconsole.log('task data changed')updateTaskThrottledCallback({ ...task, areas, roiTransformer, imagePreprocessing })}),[task, step]
)// 优化后
React.useEffect(() =>store.subscribe((s) => ({ areas: s.areas, roiTransformer: s.roiTransformer, imagePreprocessing: s.imagePreprocessing }),({ areas, roiTransformer, imagePreprocessing }) => {if (!task || step !== 1) return// 添加深度比较,避免因为对象引用变化而触发不必要的更新const hasRealChanges = JSON.stringify(task.areas) !== JSON.stringify(areas) ||JSON.stringify(task.roiTransformer) !== JSON.stringify(roiTransformer) ||JSON.stringify(task.imagePreprocessing) !== JSON.stringify(imagePreprocessing)if (!hasRealChanges) returnconsole.log('task data changed')updateTaskThrottledCallback({ ...task, areas, roiTransformer, imagePreprocessing })}),[task, step]
)
6. 区域组件渲染优化
乏精确的比较函数,自定义比较函数,只在真正需要更新时才重新渲染
const AreaItem = React.memo(({ areaIndex, area, onShowPanel }) => {// 组件逻辑
}, (prevProps, nextProps) => {// 自定义比较函数,只在真正需要更新时才重新渲染return (prevProps.areaIndex === nextProps.areaIndex &&prevProps.area.tools.length === nextProps.area.tools.length &&prevProps.area.points.length === nextProps.area.points.length &&JSON.stringify(prevProps.area.tools) === JSON.stringify(nextProps.area.tools) &&JSON.stringify(prevProps.area.points) === JSON.stringify(nextProps.area.points))
})