React Compiler 1.0 也发布大半年了。如果你还没开始用,或者试了又退回来,你可以看看这篇文章。

先说结论:这东西是一个给 React 代码做自动优化的编译时工具。你写代码,它帮你算缓存,不用再手动加 useMemo 和 useCallback。但落地过程有几个坑。

现在是什么状态

React Compiler 1.0 稳定版在 2025 年 10 月发布。到了 2026 年中,框架生态基本全覆盖了:

  • React 19+:原生支持
  • Next.js 16:开箱即用,零配置
  • Next.js 15.3.1+:通过 experimental.reactCompiler 开启
  • Expo SDK 54+:默认开启,React Native 也能用上
  • Vite 8:配置变了,下面细说

Vite 8 的配置要特别注意。 @vitejs/plugin-react v6 把内置 Babel 换成了 oxc(Rust 写的解析器),JSX 转译和 Fast Refresh 全走 Rust,速度确实快了很多。但代价是旧的 react({ babel: { plugins: [...] } }) 写法废了。现在得外挂 @rolldown/plugin-babel

// vite.config.js - 2026年新写法
import { defineConfig } from 'vite'
import react, { reactCompilerPreset } from '@vitejs/plugin-react'
import { babel } from '@rolldown/plugin-babel'

export default defineConfig({
  plugins: [
    babel({
      include: /\.[jt]sx?$/,
      babelConfig: reactCompilerPreset(),
    }),
    react(),
  ],
})

注意两件事:babel() 必须在 react() 之前;网上大半教程还在教老写法,直接抄到 Vite 8 项目里 build 会崩。

编译器实际做了什么

React 的重新渲染问题本质上是 JS 每次渲染都创建新的对象和函数引用,导致下游组件觉得"东西变了"其实根本没变。手写 useMemo 和 useCallback 就是专门治这个的。

编译器把这个问题在构建阶段就解决了。它对你的组件代码做静态分析(控制流分析 + 数据流分析 + SSA IR),然后只在实际需要的地方插入缓存,粒度比手写更细。

useMemo 举例,你手写的时候只能对整个对象缓存:

const formatted = useMemo(
  () => ({
    label: formatLabel(entity),
    total: entity.items.reduce(...),
    status: entity.state === "active" ? "green" : "red",
  }),
  [entity]
);

如果只是 entity.state 变了但 items 没变,整块还得全算一遍。编译器能对子表达式级别做缓存,只重算变化的部分。

这是一种你平时感知不到的优化,但在复杂列表渲染或派生数据多的场景下能看出差距。

18个月回顾:实际效果怎样

Sascha Becker 写了一篇很详细的 18 个月回顾,里面有几个数据值得关注:

性能数据: Meta 那边报告首屏加载快 12% 左右,页面间导航有改善,某些交互超过 2.5 倍提速。内存占用持平。

生态现状一句话总结: 新项目基本没问题(greenfield is solved),老项目是个工程(brownfield is a project)。

最大的收益不是性能,是认知负担减轻。 这不只是个口号。Meta 内部跑了一年多的反馈也类似:最大的提升不在 benchmark 数字,而在团队写代码的时候少想了一件事。

以前写组件,脑子里同时要跑两条线:业务逻辑对不对 + 这里要不要加 memo。每写一个计算就得犹豫一下。现在不用了——先写干净的代码,编译器兜底。

那些编译器处理不了的情况

违反 Rules of React

编译器假设你遵守 Rules of React。如果你不遵守,编译器不会报错——它只是静默跳过你的组件。

哪些常见操作会触发跳过?

  • render 里改了 props 或闭包变量
  • render 里读 ref.current(取 DOM 尺寸、判断元素状态)
  • 修改入参默认值
function Field({ value = defaultValue }) {
  value = value ?? fallback;  // 编译器跳过
}
  • async 组件函数

这些都是静默行为。你收不到 error,只是在 React DevTools 里看不到优化标记。

解决:装 eslint-plugin-react-compiler,在 CI 里把规则设成 error,没通过不允许合并。

旧版库兼容问题

不少老的状态管理库、表单库、拖拽库其实偷偷违反了 Rules of React,只是以前没编译检查没人发现。打开编译器后,这些库可能导致组件多渲染。

不是编译器搞坏了它们,是它暴露了它们一直存在的问题。

落地方案:分四步走

根据社区半年多的实践,推荐这样落地:

第一步:ESLint

不管用什么框架,先把 eslint-plugin-react-compiler 装上,规则设 error。跑一遍看项目有多少违规,提前摸底。

这个步骤意义很大:你能在跑编译器之前就知道它能覆盖多大比例。花十分钟就行。

第二步:annotation 模式

别一上来就全量开。官方有分级安全策略:

  • compilationMode: 'annotation':只处理标注了 "use memo" 的组件
  • compilationMode: 'infer':让编译器自己决定
  • compilationMode: 'all'(默认):全量处理

建议从 annotation 开始,挑 2-3 个关键组件试点,观察 DevTools 里渲染树有没有异常。

第三步:全量打开

确认无误后切到 all。旧的 useMemo 和 useCallback 不用急着删——编译器是增量的,跟你的手写缓存不冲突。先跑稳定再说。

第四步:清理

把那些为了防性能问题而加的 memo 逐步删掉,让代码回到最原始的形态。目标是你写业务逻辑,编译器做优化。

常见疑问:要不要删掉老代码?

这是每个团队都会问的问题。官方建议和社区实践一致:

新代码: 别写了。除非你有具体的、经过测试的理由。

老代码: 暂时留着。不要搞大扫除。删除已有的 useMemo/useCallback 可能微妙改变编译器的输出。如果某个 effect 依赖了引用稳定性,删了反而可能导致 effect 意外触发。

useMemo 和 useCallback 现在应该视为逃生阀(escape hatch),不是默认工具。需要精确控制的时候再用。

一句话总结

React Compiler 1.0 在新项目上是零成本的自动优化,在老项目上是一次需要规划的迁移。最大的收益是写代码的时候少操一份心,不是帧数提升了多少。

如果是新项目,直接用。如果是老项目,先跑 ESLint 看看情况,再决定怎么走。


本文首发于 auraimagai.com

更多推荐