【翻译】React Compiler 上线十八个月:轨迹、争议与下一步

0 阅读12分钟

React Compiler 上线十八个月:轨迹、争议与下一步

发布日期:2026 年 4 月 22 日 阅读时长:11 分钟

封面图(站点优化版本)

自从 React 19 以稳定版形态交付 Compiler 后,生态已经走过了可预期的几个阶段:框架集成、工具成熟、社区争论。本文回顾过去十八个月,也尝试解读 React 团队已经释放的后续信号。

React 19 在 2024 年底发布时,React Compiler 也一并稳定。十八个月后,生态对这类平台级变化的反应基本符合预期:先公告、再框架集成、然后工具成熟,最后进入更慢、更混乱的阶段——讨论这件事到底意味着什么。

这篇文章是对这条轨迹的回顾,也是在解读 React 团队对下一阶段的公开信号。它不是一篇入门解释文,虽然文中会包含一些解释性内容供参考。我自己没有做过大型生产迁移。下文主要基于公开文档、RFC、React 团队演讲,以及文末列出的早期采用者实践总结。

Compiler 最大的遗产不会是跑分数字,而是它淘汰的一整类 bug。“你漏了一个 useCallback 依赖”已经不再是一个讨论话题。

十八个月的轨迹

从 React Conf 2024 宣布稳定版开始,Compiler 的发展基本沿着可预期路径推进:发布、集成、工具化、争论。

发布与早期集成。 React 19 发布后的几个月里,Compiler 在实践层面的主要工作几乎都是配置。Next.js、Expo、TanStack Start,以及一批基于 Vite 的框架,都把它接进了各自的构建流水线。对新项目来说,故事变成了“默认开启,需要时可关闭”。对存量项目来说,一条以 ESLint 插件为“可迁移性前置信号”的路径逐步清晰起来。

安静的中段。 这一阶段比最初热度暗示的要安静。早期采用团队的态度更多是谨慎而非兴奋,他们报告的收益也偏“朴素”:代码评审里暴露的重复渲染问题更少,“为什么这里这么慢”这类 bug 的占比下降,代码库不再继续累积手写 memo 化蔓延。社区原本以为会大量出现的“惊艳跑分文章”并没有真正到来,因为 Compiler 最大影响是避免 bug,而不是制造标题党数字。

生态清算期。 到 2025 年末,讨论焦点从“要不要采用”转向“那些会被它搞崩的库怎么办”。多数团队目前仍卡在这里。Rules of React 原本一直成立,但现在在构建期变得可强制执行;而生态里有相当多代码其实一直在悄悄打擦边球。较老的状态库、遗留表单方案、一些拖拽实现、以及几类常用工具 Hook 都在列。Compiler 并不是“把它们弄坏了”,而是让它们“本来就有问题”这件事变得可见。

用一句话概括当前状态:绿地项目已经解决,棕地项目仍是工程。

Compiler 做了什么

简述如下,供参考。Compiler 是一个构建时转换器。它读取你的组件,假定它们遵守 Rules of React,然后在有帮助的地方插入 memo 化。Compiler 本身不引入运行时成本;输出仍然是普通 React。它的粒度通常优于人工写法:人工 useMemo 往往记忆化整个派生对象,而 Compiler 可以记忆化内部子表达式,因此改动一个字段不会让其他字段全部失效。

实践中,类似下面的代码:

const DashboardRow = memo(({ entity, onSelect }: Props) => {
  const formatted = useMemo(
    () => ({
      label: formatLabel(entity),
      total: entity.items.reduce((sum, i) => sum + i.value, 0),
      status: entity.state === "active" ? "green" : "red",
    }),
    [entity],
  );

  const handleClick = useCallback(() => {
    onSelect(entity.id);
  }, [entity.id, onSelect]);

  return (
    <Row onClick={handleClick}>
      <Label color={formatted.status}>{formatted.label}</Label>
      <Total>{formatted.total}</Total>
    </Row>
  );
});

在启用 Compiler 的代码库里,会变成像这样:

function DashboardRow({ entity, onSelect }: Props) {
  const label = formatLabel(entity);
  const total = entity.items.reduce((sum, i) => sum + i.value, 0);
  const status = entity.state === "active" ? "green" : "red";

  return (
    <Row onClick={() => onSelect(entity.id)}>
      <Label color={status}>{label}</Label>
      <Total>{total}</Total>
    </Row>
  );
}

memo 包装器、useMemouseCallback:都消失了。交互中的最直观收益,是重复渲染的扩展形态变化。典型失败案例是:列表场景里,父组件 useCallback 里一个错误依赖,会导致每次按键都让所有行重渲染;成本会随列表大小线性上升,直到出现输入卡顿。Compiler 取消了这种扩展关系。只要某一行的 props 没变,它就不会被触碰,不受列表规模影响。

为完整起见也要说成本:公开基准里,构建时间通常会上升到“几十个百分点”的量级(增量构建大多不受明显影响);由于内联 memo 化辅助逻辑,包体体积也会增加低个位数百分比。这两项都与项目有关;在你自己测量前,不要盲信任何“精确数字”。

这不是银弹。

Compiler 解决的是重复渲染问题。它不解决网络瀑布、过度抓取、包体过大,或初始 JavaScript 过慢。如果应用感到慢,先测量。很多团队在采用 Compiler 后,反而更容易看清真正瓶颈原来在别处。

Compiler 失效的地方

十八个月的生产实践让“Compiler 不会处理什么”这张清单更清晰。下面四类模式,几乎出现在我读过的每一篇迁移复盘中。

1. 在 render 期间修改 props 或闭包
function Row({ entity }: Props) {
  entity.lastSeen = Date.now(); // 在 render 期间发生可变写入
  return <span>{entity.name}</span>;
}

Compiler 会拒绝转换这类代码。你需要先修代码。

2. 在 render 期间读取 ref
function Tooltip() {
  const ref = useRef<HTMLDivElement>(null);
  const width = ref.current?.offsetWidth; // 在 render 主体里读取 ref
  return <div ref={ref}>{width}px</div>;
}

把读取操作挪到 effect 或 useLayoutEffect。Compiler 会标记这类问题,但不会替你改写。

3. 遗留 class 组件

class 组件完全不会被编译。如果你还有 React.Component 子类,它们行为和以前一样。对新项目这通常不是问题,但在老代码库里值得明确知道。

4. "use no memo" 逃生舱

有时 Compiler 会判断失误,或者把某个组件改造成“Compiler 安全”所需成本在当前阶段不值得。你可以按函数粒度选择退出:

function ComplicatedLegacyThing() {
  "use no memo";
  // Compiler 会跳过这个函数,把它当普通 React 处理
  ...
}

谨慎使用。

每一个 "use no memo" 都像是藏在代码库里的性能断崖。把它当作 TODO,而不是永久标记,并在旁边写清楚为什么这段代码暂时不满足 Compiler 安全条件。长期 grep 这个指令数量,是个很有价值的健康度指标。

大多数团队采用的迁移路径

基于 React 官方文档,以及 Next.js、Expo、TanStack Start 的集成指南,常见推荐流程很短:

  1. 先升级 React 到支持 Compiler 的版本(React 19 或更高)。
  2. 先装 ESLint 插件。 eslint-plugin-react-compiler 能在没有任何运行时变更的前提下标出 Rules of React 违规,先修这些再碰 Compiler 配置。
  3. 先启用注解驱动模式。 先编译一两个叶子组件观察效果。
  4. 再切到推断模式。 让 Compiler 自行判断处理哪些文件。跑测试,并对真实交互做性能剖析。
  5. 移除手写 memo 化。 Compiler 已接管的 useMemouseCallbackReact.memo 可以批量删除。
  6. 考虑严格模式,在代码库清理干净后启用,这样违规会抛错,而不是仅静默跳过。

跳过前几步、直接做第 5 步的团队,往往会产出一个巨大 PR,并且一开就是几周。第 1、2 步本身就能独立落地并改善代码质量。

仍在争议的问题

十八个月后,社区仍未达成一致的有三件事。

Rules of React 作为可执行契约

Rules of React 在 Compiler 出现前就存在,但那时更像“倡议”。Compiler 把它们变成了构建期边界:一旦违反,组件就会静默退出 memo 化路径。一个声音较大的少数派认为,这等于悄悄施加了比 React 早期承诺更严格的编程模型。反方观点是:这些规则本来就不是真可选项;Compiler 只是把后果显性化。两边都部分成立,而这种张力在那些早于“规则成文化”时代的老库中尤为明显。

"use no memo" 作为永久技术债

这个逃生舱太容易用,这让那些见过代码库多年累积类似标记的人感到担忧。常见类比是 @ts-ignore// eslint-disable:它们是有价值的减压阀,但一旦没人回看,老化速度会很快。另一派则认为这个指令之所以关键,正因为它允许你不做全量重构也能继续交付;把它当永久标记是误用,不是功能本身的问题。如今几乎没人争议的一点是:"use no memo" 的数量是合法且有意义的代码库健康指标。

Compiler 与运行时优化器

对大多数应用,Compiler 已够用。但对列表极重的专业 UI(交易面板、日志查看器、电子表格),像 Million.js 这种基于 block 的协调器仍有可测的优势。两层方案解决的是不同问题:Compiler 降低组件多久重渲染一次,运行时优化器改变每次重渲染有多快。一些团队会两者并用。它们之间的实际交互总体没问题,但还没有形成一份真正“尘埃落定”的文档化共识。我猜这会是未来某篇“定版文章”的主题。

接下来会发生什么

从 React 团队公开信息、Meta 工程文章和活跃 RFC 看,后续方向大致有五个。它们都不是承诺,更像方向信号。

更细粒度的编译控制

当前模式较粗:开、关、注解驱动。团队真正想要的似乎是组件级提示:这个组件激进编译,那个保守编译,这片遗留岛屿永不编译;再加上更好的可视化,让你看到 Compiler 实际做了什么。预计会出现一些能收窄当前“全开/全关”权衡的指令能力。

Compiler 感知的 Server Components

RSC 已经能为客户端岛屿产出更有利于 memo 化的输出。下一步可能是收紧序列化边界:当 Compiler 能证明某值不必跨边界时,浏览器需要 hydration 的负载会更小。真实应用里最大的性能收益常在这里:降低 hydration 成本通常比降低重渲染次数更关键,因为冷启动时用户最先感知到的就是 hydration。

useEvent 走向收敛

那个“长期讨论中的原语”——能读取最新状态、同时保持事件回调稳定的 useEvent——已经在 RFC 里讨论了多年。Compiler 的纯度分析是让其语义可验证的关键,这也是为什么该提案在 Compiler 出现前长期停滞。预计它会以某种形式落地。

React Native

原生视图 diff 的成本通常高于 Web 端协调,所以自动 memo 化在这里的边际价值更大。Expo 已经交付了 Compiler 支持;React Native 本体推进相对更慢。随着“每个组件收益更大”这一事实被更多团队验证,追求对齐的压力在上升。

开发者工具

当前缺的一块,是一个能按组件展示“Compiler 做了什么、为什么这么做”的 DevTools 面板。现在 ESLint 插件能告诉你“哪些被跳过”,但如果有一个“哪些被优化了”的可视化器,会像 RSC 树可视化器那样,把 Compiler 的工作变得可理解。就我所知,这不在任何公开路线图上,但它是最显然的下一步,也是社区插件可能先于 React 团队做出来的那类工具。

我对后续路线的看法

十八个月后,Compiler 的遗产不会是跑分数字,而是它淘汰的一整类 bug。“你漏了一个 useCallback 依赖”已经不再是对话内容。同样,“这个组件要不要 memo 化?”也不再是问题。答案永远是要,而 Compiler 会处理它。

更有意思的问题是:当规则被编译器强制执行后,Rules of React 会变成什么。过去很多年,它们更像“好 React 代码通常会做的事”;现在它们是构建期契约,不满足就会被拒绝。生态里一些最老的库仍在适应。未来一年里,Compiler 故事的重点也许不在 Compiler 本身,而在“当所有东西都被改到 Compiler-safe 后,库生态会变成什么样”。

对新项目,决策很简单:直接开启。对存量项目,决策在于你是要修最老那批代码,还是把 "use no memo" 当作一种承诺装置先交付。这两种都是真实 trade-off,也都值得有意识地做。

在 2026 年再看到 useMemouseCallback 时,可以把它看作现代 JavaScript 里手写 for 循环:通常没问题,偶尔确实必要,但更多时候只是说明这段代码写于更好工具出现之前。

术语表(本篇命中)

英文术语译文说明
React CompilerReact CompilerReact 编译器名称,专有名词保留英文
Rules of ReactReact 规则(Rules of React)编译器依赖的代码约束集合
memoizationmemo 化指通过缓存避免重复计算或重复渲染
hydrationHydration(激活)客户端接管服务端渲染输出的过程
brownfield棕地项目指带历史包袱的存量系统