前端开发 · 2026年3月31日

React Compiler正式稳定了:删掉你项目里一半的useMemo和useCallback

React性能优化这件事,前端写了多少年了?从shouldComponentUpdate到React.memo,从useMemo到useCallback,每个项目里都有一堆手动memo代码。写的时候觉得自己在做优化,review的时候发现一半都是无效的——依赖数组写错、memo了不该memo的值、或者压根没测过是不是真的有性能问题就先memo了再说。

Next.js 16把React Compiler标记为stable了。一行配置,编译器在build阶段自动分析你的组件,该memo的memo,不该memo的跳过。手写useMemo和useCallback的时代,可能真的要结束了。

React Compiler是什么

不是什么新runtime,不是什么框架魔法。它是一个Babel transform插件,在构建阶段跑。

工作流程:读你的组件源码 → 分析每个表达式的依赖关系 → 自动插入memo逻辑 → 输出标准JS。运行时零开销,不引入任何新的依赖。

Meta内部用了好几年了,Facebook和Instagram的生产环境一直在跑一个叫React Forget的内部版本。现在提取出来开源,在Next.js 16里正式标为stable。

它具体做了什么

三个层级的自动优化:

组件级:如果组件的props没变,整个渲染跳过——等价于你手写React.memo()。

值级:组件内部的计算表达式,如果依赖的props/state没变,返回缓存结果——等价于useMemo。

回调级:传给子组件的函数,如果闭包捕获的变量没变,引用保持稳定——等价于useCallback。

看个具体例子。你写的代码完全不用改:

function ProductCard({ price, quantity, onAdd }) {
  const total = price * quantity;
  const formatted = `¥${total.toFixed(2)}`;

  return (
    <div className="card">
      <span>{formatted}</span>
      <button onClick={() => onAdd(total)}>加入购物车</button>
    </div>
  );
}

编译器输出(简化后)大概长这样:

const ProductCard = memo(function({ price, quantity, onAdd }) {
  const total = useMemo(() => price * quantity, [price, quantity]);
  const formatted = useMemo(() => `¥${total.toFixed(2)}`, [total]);
  const handleClick = useCallback(() => onAdd(total), [onAdd, total]);

  return (
    <div className="card">
      <span>{formatted}</span>
      <button onClick={handleClick}>加入购物车</button>
    </div>
  );
});

它不是无脑把每个表达式都包一层memo。编译器做了逃逸分析——传给event handler的值、存到ref里的值、丢给外部store的值,处理方式都不一样。只有能证明安全的地方才会插入memo。

怎么开启

Next.js 16里两步搞定:

npm install --save-dev babel-plugin-react-compiler
npm install --save-dev eslint-plugin-react-compiler
// next.config.ts
const nextConfig = {
  experimental: {
    reactCompiler: true,
  },
};

export default nextConfig;

就这么多。不需要改任何组件代码。

如果你胆子没那么大,可以先用annotation模式渐进式开启:

experimental: {
  reactCompiler: {
    compilationMode: "annotation",
  },
}

annotation模式下,只有文件顶部加了"use memo"指令的组件才会被编译器处理。验证没问题后再全局开启。

在真实项目上试了一把

我拿了一个中等复杂度的后台管理系统测试——大概60个页面,大量表格、表单、图表组件。用的Next.js 15,升级到Next.js 16后开启React Compiler。

测试环境:MacBook Pro M4 Pro, Chrome 134, React DevTools Profiler

先看构建:

指标 开启前 开启后 变化
构建时间 42s 48s +14%
bundle大小 1.82MB 1.87MB +2.7%

构建慢了6秒,bundle大了50KB左右。在预期范围内——编译器插入的memo代码总要占点体积。

再看运行时,这才是重点:

场景 开启前 开启后 变化
Dashboard首屏render 18个组件重渲染 7个组件重渲染 -61%
表格翻页 整个页面re-render 只有表格区域re-render
表单输入(单字段) 全表单12个字段re-render 只有当前字段re-render
图表时间筛选 6个图表全部re-render 2个数据变化的图表re-render -66%

效果最明显的是表单场景。之前在一个字段里打字,React DevTools Profiler里能看到整个表单的所有字段都在闪——因为表单状态在父组件里,一更新就全部re-render。开启编译器后,只有正在输入的字段和依赖它的校验提示在更新。

不过有个意外:项目里有3个组件被编译器跳过了。ESLint插件报了warning——这几个组件在render里直接修改了外部变量(违反了Rules of React的纯函数规则)。编译器遇到不确定安全的代码就直接跳过,不会强行优化导致bug。这个设计比较稳。

踩坑记录

坑1:某些第三方库的HOC不兼容

项目里用了一个老版本的状态管理库,它的connect HOC内部用了对象引用相等性检查。编译器优化后引用变了,导致组件不更新。

解决:给这个HOC包裹的组件加"use no memo"指令跳过编译。

function MyConnectedComponent(props) {
  "use no memo";
  // 编译器不碰这个组件
  return <div>{props.data}</div>;
}

坑2:useEffect里依赖编译器memo后的值,行为不同

有个组件在useEffect里依赖一个对象,之前每次render都是新对象(引用不同),所以effect每次都执行。编译器memo后引用稳定了,effect不触发了。

这其实暴露了原来代码的一个隐藏bug——依赖了引用变化来触发副作用,本身就是错误的模式。修正了依赖数组后反而更正确了。

坑3:排除目录的配置

项目里有些generated代码不需要编译器处理,可以用excludeDirectories排除:

reactCompiler: {
  excludeDirectories: ["./src/generated", "./src/legacy"],
}

能不能删掉已有的useMemo/useCallback

能,但不急。

编译器和手动memo是共存的——你不删也不会出问题。编译器足够聪明,看到你已经手动memo了,它就不会再包一层。

推荐的节奏:

1. 先全局开启编译器,跑完整测试
2. 用eslint-plugin-react-compiler扫一遍,它会告诉你哪些手动memo是多余的
3. 一个文件一个文件地清理,别一口气全删

我在项目里试着删了一部分。60个页面里大概有200+个useMemo和150+个useCallback调用。eslint插件标记了其中约70%是可以安全删除的。清理完后代码清爽了不少,该有的性能优化编译器都给你做了。

什么项目适合立刻开

收益大的场景

  • 数据Dashboard——大量图表共享筛选条件,一个筛选变化本来会触发全部图表re-render
  • 复杂表单——字段多、联动多、校验规则多
  • Feed流/列表——实时数据更新,只有变化的item需要re-render
  • 用了React Context做全局状态的项目——Context consumer之前只要context变就全re-render

收益小的场景

  • 已经手动优化得很彻底的项目
  • 组件树很浅、状态更新不频繁的页面
  • 性能瓶颈在网络/接口响应而不在渲染的项目

对前端架构的影响

React Compiler稳定意味着一件事:性能优化的心智负担从开发者转移到了编译器。

以前做code review的时候经常看到两种极端——要么完全不memo(”反正用户感知不到”),要么疯狂memo(”万一以后性能有问题呢”)。现在这个争论可以结束了:不用手动管,编译器比你精确。

这也意味着React的Rules——纯函数组件、不可变props、hooks顶层调用——从”最佳实践”变成了”硬性要求”。不遵守规则的组件编译器直接跳过,等于拿不到免费的性能优化。对团队规范来说反而是好事:不守规矩有代价了。

Next.js 16 + React Compiler stable,前端项目的性能基线又被拉高了一档。早开早享受,反正就一行配置的事。