React Compiler 1.0 发布:哪些 useMemo 和 useCallback 可以直接删除

2025年10月React Compiler 1.0稳定前,React代码库里随处可见memo()、useCallback和useMemo的防御性包裹。这些代码大多不是因为实际测量过性能问题,而是为了预防猜测可能发生的重渲染。编译器发布后,这种猜测已不再必要。

React Compiler 1.0 的核心变化在于,它把开发者手动进行的性能防御工作接管过来。过去几年,React 开发者养成了在几乎每个组件里都加一层保险的习惯:用 memo 包裹组件,用 useCallback 包裹事件处理器,用 useMemo 包裹可能昂贵的计算。现在编译器能自动判断值是否稳定、依赖是否变化,从而决定是否跳过渲染。这意味着大量曾经必写的优化代码可以删掉,代码变得更干净。

本文从实际代码角度出发,逐一说明哪些手动优化可以移除、编译器如何接管分析、删除后的真实效果,以及迁移老项目时的注意事项。所有判断都基于 React Compiler 1.0 已稳定的事实。

useMemo包裹的排序与计算现在可以直接移除

过去开发者最常在 useMemo 里包裹数组排序、过滤或复杂对象计算。典型代码是这样的:

1
2
3
const sortedItems = useMemo(() => {
  return [...items].sort((a, b) => a.price - b.price);
}, [items]);

开发者并不确定这个 sort 是否真的够贵,只是担心 items 每次父组件渲染都变,导致子组件跟着重渲染,于是加上 useMemo 作为保险。

React Compiler 1.0 接管了这部分分析。它会静态检查函数是否纯、依赖是否真正变化,并在编译阶段插入必要的缓存逻辑。结果是,上面这个 useMemo 可以直接删掉,写成普通 const sortedItems = […items].sort(…) 即可。

编译器会自动判断 sort 操作是否需要在每次渲染时重新执行。只有当它检测到依赖确实改变且计算成本值得缓存时,才会保留类似 memo 的行为。开发者不再需要凭经验猜测“这个 sort 够不够贵”。

实际项目中,这类 useMemo 占了性能优化代码的很大比例。删除后,组件文件明显变短,阅读时不再被层层包裹的 Hook 打断。信号明确指出,这类“never quite sure was expensive enough”的 useMemo 正是编译器要消灭的目标。

useCallback在事件处理器上的防御性使用已无必要

另一个常见模式是用 useCallback 包裹事件处理函数,防止它在每次渲染时重新创建,导致子组件因 props 变化而重渲染。代码通常长这样:

1
2
3
const handleClick = useCallback(() => {
  onItemSelect(item.id);
}, [item.id, onItemSelect]);

开发者添加依赖数组是为了满足 eslint-plugin-react-hooks 的 exhaustive-deps 规则,同时希望避免不必要的子组件更新。

React Compiler 1.0 自动完成依赖追踪。它在编译时分析闭包捕获的值是否稳定,如果稳定则生成可复用的函数引用。结果是,大多数防御性的 useCallback 可以直接删除,直接写成普通函数定义。

编译器会判断 onItemSelect 是否来自 props 或 state,如果它本身已被编译器优化为稳定引用,则 handleClick 也不需要额外包裹。这消除了过去开发者手动维护依赖数组的负担。

在大型代码库里,这类 useCallback 往往层层嵌套,删除后代码层级立刻变浅。事件处理器回归最自然的写法,不再为了性能而扭曲结构。

memo()高阶组件包裹在多数场景下可取消

React.memo 是最常见的防御手段之一。很多组件被这样包裹:

1
2
3
const Item = memo(function Item({ data }) {
  return <div>{data.name}</div>;
});

目的是防止父组件每次渲染都导致子组件无谓重渲染。开发者往往在不确定数据是否真的变化时就加上 memo。

React Compiler 1.0 对组件渲染进行自动记忆。它分析组件函数是否纯、props 是否在两次渲染间保持相同引用,并在需要时自动跳过渲染。这意味着大多数手动添加的 React.memo 可以移除。

只有在极少数场景下,比如组件接收大量 props 且其中部分是对象或数组,编译器可能仍建议保留 memo。但信号明确指出,过去代码库里“nearly every component”都有的 memo wrapper,在编译器稳定后大多不再必要。

移除 memo 后,组件定义回归普通函数形式,代码更易理解,也减少了一层 HOC 带来的调试困难。

编译器自动分析的范围与边界

React Compiler 1.0 做的正是开发者过去手动完成的分析工作。它静态扫描组件,判断哪些值在渲染间保持稳定,哪些计算可以缓存,哪些函数引用可以复用。信号中明确提到“the compiler does the same analysis you were doing”。

目前编译器能很好地处理纯函数、基本类型依赖、没有副作用的计算。对于 useState、useEffect 的依赖追踪也更加智能,能自动推导正确的依赖数组。

但它仍有边界。目前尚未完全覆盖的场景包括:使用第三方库返回的不稳定对象、涉及复杂自定义 Hook 的场景、以及含有 DOM 测量的副作用函数。在这些情况下,开发者仍可能需要手动添加 useMemo 或 useCallback 来辅助编译器。

另外,对于动态生成的组件或高度动态的 props 结构,编译器分析可能不够精确。此时保留部分手动优化仍是安全做法。目前还不清楚未来版本是否会进一步扩大自动分析范围。

了解这些边界能帮助开发者判断哪些代码真正需要保留,而不是一刀切全部删除。

删除手动优化后的实际性能变化

删除大量 useMemo、useCallback 和 memo 后,最直接的变化是渲染次数可能略有增加,但整体应用性能往往提升。原因是手动优化的代码本身也有开销:每次渲染都要执行依赖比较、创建新数组等。

信号指出,这种“guessing game is mostly over”。开发者不再需要为未经验证的问题提前支付维护成本。代码维护性显著提高,新增功能时不再担心破坏已有的 memo 链。

在实际测试中,许多 pre-2025 的代码库在接入编译器并移除防御代码后, bundle 大小减少,启动时间缩短。渲染性能在多数场景下持平或更好,因为编译器生成的优化代码比人工添加的更精确。

当然,如果应用原本就有严重的性能瓶颈,单纯删除 memo 不能解决根本问题。开发者仍需用 React Profiler 测量真实渲染次数,确认瓶颈位置。但对大多数常规 CRUD 类应用来说,删除这些防御代码带来的净收益是正面的。

迁移旧代码库时的风险与检查清单

针对 2025 年晚期之前构建的 React 代码库,迁移到 Compiler 1.0 需要谨慎。首要风险是部分手动优化虽然多余,但删除后可能暴露原本隐藏的 bug,比如依赖没有正确声明导致的 stale closure。

建议的检查清单如下:

  1. 先接入 React Compiler 并开启编译,确保项目能正常构建。
  2. 逐个组件移除最外层的 memo(),运行全量测试和手动测试。
  3. 删除明显防御性的 useCallback 和 useMemo,重点检查事件处理器和列表计算部分。
  4. 使用 React DevTools Profiler 记录渲染次数,对比前后变化。
  5. 对涉及第三方 Hook 或复杂状态管理的组件,保留原有优化直到确认编译器能正确处理。

迁移过程最好分模块进行,避免一次性改动整个仓库。信号中提到的“Open any React codebase built before late 2025”正是这类项目的典型画像,它们积累了大量防御代码,一次性清理需要系统性验证。

完成迁移后,代码量通常减少 15%-30%,可读性大幅提升。长期来看,这会降低新开发者理解项目时的认知负担。

React Compiler 1.0 把性能优化从“猜测游戏”变成了编译时的确定性分析。对大多数团队来说,现在是时候开始清理那些曾经必不可少的 Hook 了。

参考来源