React 的默认行为是「状态变化 → 组件及其子树重渲染」。重渲染本身很便宜,贵的是渲染过程中做的计算与 DOM 操作。所以优化的第一步不是加 memo,而是找出真正贵的地方。

先定位,再优化
- 打开 React DevTools 的 Profiler,录制一次交互;
- 在 flamegraph 里找耗时最高的组件,看「为什么渲染」一栏的触发源;
- 只有确认某个组件被无关状态反复带动,才值得做隔离。
没有测量就加 memo,最常见的结局是依赖数组写了一堆,界面依旧卡。
收益最高:把状态放到真正需要它的地方
function Page() {
const [keyword, setKeyword] = useState('');
return (
<>
<SearchInput value={keyword} onChange={setKeyword} />
<ExpensiveTable />
</>
);
}
输入框的每次按键都会让 Page 重渲染,连带整张表格。把 keyword 移进 SearchInput 内部,只在提交时回调出去,父组件状态不变,表格就不会被带动。这类「状态下放」几乎不需要额外 API,收益却最大。
次高:把子树作为 children 传入
function Layout({ children }) {
const [collapsed, setCollapsed] = useState(false);
return (
<div className={collapsed ? 'w-16' : 'w-64'}>
<Sidebar onToggle={() => setCollapsed(c => !c)} />
{children}
</div>
);
}
children 是父组件渲染时创建好的元素对象,Layout 自身状态变化不会重新创建它,React 会直接复用。侧边栏这类「只切换自己 UI」的容器组件特别适合这种写法。
再考虑 memo
const Row = memo(function Row({ item, onSelect }) {
return <tr onClick={() => onSelect(item.id)}>{item.name}</tr>;
});
memo 做的是浅比较,两个前提必须满足:props 真的没变,且比较成本低于渲染成本。最常见的失效原因是父组件每次都传新对象或新函数,此时要么用 useCallback / useMemo 固定引用,要么把回调改到子组件内部处理。
const handleSelect = useCallback((id) => setSelected(id), []);
const options = useMemo(() => buildOptions(raw), [raw]);
useMemo 不是默认选项
它自身要保存依赖、做比较,对简单计算反而是负担。判断标准:计算量明显(大数组排序、复杂格式化、递归)或结果作为依赖被别的 Hook 使用,才值得包。useMemo 也不保证一定缓存,只能当性能提示,不要用它承担正确性。
列表:key 与组件粒度
用稳定 id 作 key;列表项组件尽量小,把「只有某一项会变」的状态留在项内部。整表重排时 key 变化会让 React 重建 DOM,代价远高于一次重渲染。
小结
优化顺序:状态下放 → children 组合 → memo 隔离 → useMemo 缓存计算。反过来做,往往得到一堆依赖数组却看不到提升。Hooks 依赖本身的问题见依赖数组与闭包陷阱。