react性能优化-重新render
props与memo
如下所示例子中,因为App内部状态的更新,总会牵连其无辜子组件Demo的更新。
const Demo = () => {
console.log('Demo render');
return (
<div>
我是子组件
</div>
);
};
const App = () => {
console.log('App render');
const [count, setCount] = useState(0);
const doInc = () => {
setCount(count + 1);
};
return (
<>
<div>{count}</div>
<button onClick={doInc} className='bg-blue-400 p-1 rounded text-white'>改变组件</button>
<hr className='mt-2'/>
<Demo />
</>
);
};
如何避免呢? 其实官方已经提供了这个优化的方案,那就是使用memo,你仅仅且包裹住这个组件即可!
memo的作用就是对比组件重新render前后的 props是否发生变化:如果没有发生变化,则不会重新render。
const Demo = memo(() => {
console.log('Demo render');
return (
<div>
我是子组件
</div>
);
});
但是注意memo 仅比较基本类型,如果是对象则会比较引用地址,如下则每次还都会重新render:
const Demo = memo((props) => {
console.log('Demo render');
return (
<div>
我是子组件
</div>
);
});
const App = () => {
console.log('App render');
const [count, setCount] = useState(0);
const doInc = () => {
setCount(count + 1);
};
const info = {title: '子组件'};
return (
<>
<div>{count}</div>
<button onClick={doInc} className='bg-blue-400 p-1 rounded text-white'>改变组件</button>
<hr className='mt-2'/>
<Demo info={info}/>
</>
);
};
原因也很简单,因为每次App组件的重新render,就会导致info是一个新创建的对象,引用地址自然就不同了!
解决办法:自己做对象比较就行!
import { memo, useState } from 'react';
import isEqual from "lodash/isEqual";
const Demo = memo(() => {
console.log('Demo render');
return (
<div>
我是子组件
</div>
);
},(prev, next)=>isEqual(prev, next));
const App = () => {
console.log('App render');
const [count, setCount] = useState(0);
const doInc = () => {
setCount(count + 1);
};
const info = {title: '子组件'};
return (
<>
<div>{count}</div>
<button onClick={doInc} className='bg-blue-400 p-1 rounded text-white'>改变组件</button>
<hr className='mt-2'/>
<Demo info={info}/>
</>
);
};
如果你觉得麻烦,还可以使用另一种方式来规避这种情况。
使用useMemo包裹住这个对象即可,这样这个对象就会被缓存下来不会被App重建!
const Demo = memo((props) => {
console.log('Demo render');
return (
<div>
我是子组件
</div>
);
});
const App = () => {
console.log('App render');
const [count, setCount] = useState(0);
const doInc = () => {
setCount(count + 1);
};
const info = useMemo(()=>({title: '子组件'}),[]);
return (
<>
<div>{count}</div>
<button onClick={doInc} className='bg-blue-400 p-1 rounded text-white'>改变组件</button>
<hr className='mt-2'/>
<Demo info={info}/>
</>
);
};
tips: useMemo一般用于缓存对象,如果是函数react还提供了useCallback,作用一样!
useCtx与memo
有时候父子尤其是跨级传递参数,我们并不想通过props传递,此时便是使用useContext的时候
// 定义store
export const defaultValue = {
count: 0
};
export const AppCtx = createContext(null);
export function useAppCtx() {
const ctx = useContext(AppCtx);
return ctx;
}
// 子组件
// 子组件必须包裹memo,因为父组件状态变态变更会牵连到它,要去除这种牵连
const Demo = memo(() => {
console.log('Demo render');
const { app } = useAppCtx(); // 要不要重新render跟app没关系,哪怕不写左侧内容
return <div>
我是子组件
<div>{JSON.stringify(app)}</div>
</div>;
});
// 父组件
const App = () => {
console.log('App render');
const [app, setApp] = useState(defaultValue);
const doInc = () => {
setApp({ ...app, count: app.count+1});
};
return (
<AppCtx value={ app }>
<div>{app.count}</div>
<button onClick={doInc} className="bg-blue-400 p-1 rounded text-white">
改变count
</button>
<hr className="mt-2" />
<Demo />
</AppCtx>
);
};
但是我们会遇到一个问题:因为设置app每次都为一个新的对象,这导致useContext认为是新的值,从而导致子组件重新渲染!
如下,哪怕在app中我没次都设置count的值为初始值(即从始至终 app内容都没变过),但是子组件依然会重新渲染!
const App = () => {
console.log('App render');
const [app, setApp] = useState(defaultValue);
const doInc = () => {
setApp({ ...app, count: 0});
};
return (
<AppCtx value={ app }>
<div>{app.count}</div>
<button onClick={doInc} className="bg-blue-400 p-1 rounded text-white">
改变组件
</button>
<hr className="mt-2" />
<Demo />
</AppCtx>
);
};
其实解决的办法也还是通过useMemo来解决
// 父组件
const App = () => {
console.log('App render');
const [app, setApp] = useState(defaultValue);
const doInc = () => {
setApp({ ...app, count: 0});
};
// 如此appCtxValue对象根据条件做缓存(即固化)
const appCtxValue =useMemo(() => ({
app
}), [app.count]);
return (
<AppCtx value={ appCtxValue }>
<div>{app.count}</div>
<button onClick={doInc} className="bg-blue-400 p-1 rounded text-white">
改变组件
</button>
<hr className="mt-2" />
<Demo />
</AppCtx>
);
};
memo的比较规则
useMemo
(以及 useCallback
、useEffect
等所有 Hook 的 deps 数组)只认“严格相等(Object.is
)”这一层,它不会、也永远不会对“对象里的字段”做深度比较。
所以“内容一样但引用不同”的对象,在 deps 里一定会被认为是“变了”,于是 useMemo
会重新执行。
一、为什么设计成这样
- 可预测性:
如果 React 帮你做深比较,内部就要遍历所有字段,遇到循环引用、函数、Symbol 等情况还得特殊处理,结果仍然可能和开发者预期不一致。 - 性能:
深比较成本远高于“全等比较”,而 deps 数组在每次渲染时都会被遍历一次;
把“是否相等”的决定权交给开发者,可以让大家在“稳定引用”与“重新计算”之间自由权衡。 - 数据流哲学:
React 希望你把“变化”建模成不可变数据——只要语义没变,就复用同一个引用;语义变了,就生成新引用。这样===
本身就等价于“语义是否变化”。
二、常见“踩坑”复现
function Comp() {
const [count, setCount] = useState(0);
// 每次渲染都生成新的「相同内容」的对象
const params = { page: 1, size: 10 };
const data = useMemo(() => {
console.log('重新 fetch');
return api.fetch(params);
}, [params]); // ← 这里永远是“新引用”,所以 useMemo 永远失效
return (
<>
<button onClick={() => setCount(c => c + 1)}>rerender</button>
<div>{data}</div>
</>
);
}
控制台会看见“重新 fetch”随每次点击都打印,尽管 page/size
根本没变。
三、4 种正统解决方式
- 把对象拆成原始值
最简单、零成本,React 官方推荐:
const data = useMemo(() => api.fetch({ page, size }), [page, size]);
- 用
useMemo
把“对象引用”本身稳住
只要字段没变,就给它同一个引用:
const params = useMemo(() => ({ page: 1, size: 10 }), []); // 空 deps => 永不变
const data = useMemo(() => api.fetch(params), [params]);
- 用
useDeepCompareMemo
之类自定义 Hook(内部帮你深比较)
社区已有轮子:use-deep-compare-effect
、use-deep-compare-memo
原理:第一次必执行;后续用深比较决定是否替换 deps,从而触发真正的 React hook。
import useDeepCompareMemo from 'use-deep-compare-memo';
const data = useDeepCompareMemo(() => api.fetch(params), [params]);
- 用 immer / zustand / redux-toolkit 等“不可变更新”工具
只要业务数据语义没变,就天然返回旧引用;语义变了就返回新引用,
这样直接用===
就能判断,无需深比较。
// 父组件
const App = () => {
console.log('App render');
const [app, setApp] = useImmer(defaultValue);
const doInc = () => {
// 当defaultValue为对象时,仅仅当内容发生变化时才会设置为新对象,否则依然是原对象
setApp(draft => {
draft.count = 0;
});
};
return (
<AppCtx value={ app }>
<div>{app.count}</div>
<button onClick={doInc} className="bg-blue-400 p-1 rounded text-white">
改变组件
</button>
<hr className="mt-2" />
<Demo />
</AppCtx>
);
};
四、一句话总结
useMemo
的 deps 只浅比较引用;
想让“内容相同就跳过计算”,要么拆成原始值,要么维持对象引用稳定,要么用自定义 Hook 做深比较。
这是 React 不可变数据模型的核心约定,不是“缺陷”,而是“设计如此”。
react-compiler
近期(2025年)提出了一个新的优化性能方式:react-compiler。
只要配置好这个,你无需任何手动使用memo、useMemo、useCallback优化,react会自己优化!
官方说法:React Compiler 会自动为你完成这项优化,让你从这些繁琐的负担中解脱出来,专注于功能构建。