React 编译器

本页面将向您介绍新的实验性 React 编译器以及如何成功试用它。

正在施工中

这些文档仍在编写中。更多文档可在 React 编译器工作组仓库 中找到,并且会在它们更稳定时上传到这些文档中。

您将学习

  • 编译器入门
  • 安装编译器和 eslint 插件
  • 故障排除

注意

React 编译器是一款新的实验性编译器,我们已将其开源以从社区获得早期反馈。它仍然存在一些问题,尚未完全准备好用于生产环境。

React 编译器需要 React 19 RC 版本。如果您无法升级到 React 19,您可以尝试 工作组 中所述的缓存函数的用户空间实现。但是,请注意,不建议这样做,您应该尽可能升级到 React 19。

React 编译器是一款新的实验性编译器,我们已将其开源以从社区获得早期反馈。它是一个仅在构建时运行的工具,可以自动优化您的 React 应用程序。它适用于纯 JavaScript,并且理解 React 规则,因此您无需重写任何代码即可使用它。

该编译器还包括一个 eslint 插件,可以直接在您的编辑器中显示来自编译器的分析结果。该插件独立于编译器运行,即使您的应用程序中没有使用编译器,也可以使用它。我们建议所有 React 开发人员都使用此 eslint 插件来帮助提高代码库的质量。

编译器有什么作用?

为了优化应用程序,React 编译器会自动对您的代码进行记忆化。您可能已经通过 useMemouseCallbackReact.memo 等 API 了解了记忆化。使用这些 API,您可以告诉 React 应用程序中的某些部分如果其输入没有更改,则无需重新计算,从而减少更新时的工作量。虽然功能强大,但很容易忘记应用记忆化或错误地应用它们。这会导致低效的更新,因为 React 必须检查 UI 中没有任何更改的部分。

编译器利用其对 JavaScript 和 React 规则的了解,自动记忆组件和钩子中的值或值组。如果它检测到规则被破坏,它将自动跳过这些组件或钩子,并继续安全地编译其他代码。

如果您的代码库已经进行了很好的记忆化,那么您可能不会期望使用编译器后性能会有重大提升。但是,在实践中,手动正确记忆化导致性能问题的依赖关系非常棘手。

深入探讨

React 编译器添加了哪些类型的记忆化?

React 编译器的初始版本主要侧重于提高更新性能(重新渲染现有组件),因此它侧重于以下两个用例

  1. 跳过组件的级联重新渲染
    • 重新渲染 <Parent /> 会导致其组件树中的许多组件重新渲染,即使只有 <Parent /> 发生了变化
  2. 跳过 React 之外的昂贵计算
    • 例如,在需要该数据的组件或钩子内部调用 expensivelyProcessAReallyLargeArrayOfObjects()

优化重新渲染

React 允许您将 UI 表示为其当前状态(更具体地说:它们的 props、state 和 context)的函数。在当前实现中,当组件的状态发生变化时,React 会重新渲染该组件*及其所有子组件*——除非您使用 useMemo()useCallback()React.memo() 应用了某种形式的手动记忆化。例如,在以下示例中,每当 <FriendList> 的状态发生变化时,<MessageButton> 都会重新渲染。

function FriendList({ friends }) {
const onlineCount = useFriendOnlineCount();
if (friends.length === 0) {
return <NoFriends />;
}
return (
<div>
<span>{onlineCount} online</span>
{friends.map((friend) => (
<FriendListCard key={friend.id} friend={friend} />
))}
<MessageButton />
</div>
);
}

在 React 编译器Playground 中查看此示例

React 编译器会自动应用等效的手动记忆化,确保只有应用程序的相关部分会随着状态的变化而重新渲染,这有时被称为“细粒度响应性”。在上面的示例中,React 编译器确定即使在 friends 发生变化时,<FriendListCard /> 的返回值也可以被重用,并且可以避免重新创建此 JSX *以及*避免在计数变化时重新渲染 <MessageButton>

昂贵的计算也会被记忆化

编译器还可以自动记忆化渲染过程中使用的昂贵计算。

// **Not** memoized by React Compiler, since this is not a component or hook
function expensivelyProcessAReallyLargeArrayOfObjects() { /* ... */ }

// Memoized by React Compiler since this is a component
function TableContainer({ items }) {
// This function call would be memoized:
const data = expensivelyProcessAReallyLargeArrayOfObjects(items);
// ...
}

在 React 编译器Playground 中查看此示例

但是,如果 expensivelyProcessAReallyLargeArrayOfObjects 确实是一个昂贵的函数,您可能需要考虑在 React 之外实现它自己的记忆化,因为

  • React 编译器仅记忆化 React 组件和钩子,而不是每个函数。
  • React 编译器的记忆化不会在多个组件或钩子之间共享。

因此,如果 expensivelyProcessAReallyLargeArrayOfObjects 在许多不同的组件中使用,即使传递了完全相同的项目,也会重复运行该昂贵计算。我们建议先进行性能分析,以确定它是否真的如此昂贵,然后再使代码变得更复杂。

编译器假设什么?

React 编译器假设您的代码

  1. 是有效的、语义化的 JavaScript。
  2. 在访问可为空/可选的值和属性之前,会测试它们是否已定义(例如,如果使用 TypeScript,则通过启用 strictNullChecks),即 if (object.nullableProperty) { object.nullableProperty.foo } 或使用可选链式 object.nullableProperty?.foo
  3. 遵循React 规则

React 编译器可以静态验证许多 React 规则,并在检测到错误时安全地跳过编译。要查看错误,我们建议您也安装 eslint-plugin-react-compiler

我应该试用编译器吗?

请注意,编译器仍处于实验阶段,还有许多粗糙的边缘。虽然它已经在 Meta 等公司投入生产,但在您的应用程序中将编译器推向生产环境将取决于您的代码库的健康状况以及您对 React 规则的遵循程度。

您现在不必急于使用编译器。可以在它达到稳定版本后再采用它。但是,我们确实感谢您在应用程序的小型实验中试用它,以便您能够向我们提供反馈,帮助我们改进编译器。

入门

除了这些文档之外,我们还建议您查看React 编译器工作组,以获取有关编译器的更多信息和讨论。

检查兼容性

在安装编译器之前,您可以先检查您的代码库是否兼容。

终端
npx react-compiler-healthcheck@latest

此脚本将

  • 检查可以成功优化的组件数量:越高越好。
  • 检查 <StrictMode> 的使用情况:启用并遵循此选项意味着更有可能遵循React 规则
  • 检查不兼容的库使用情况:已知与编译器不兼容的库。

例如

终端
已成功编译 9 个组件中的 8 个。未找到 StrictMode 使用情况。未找到使用不兼容库的情况。

安装 eslint-plugin-react-compiler

React 编译器还为 eslint 插件提供支持。eslint 插件可以*独立*于编译器使用,这意味着即使您不使用编译器,也可以使用 eslint 插件。

终端
npm install eslint-plugin-react-compiler

然后,将其添加到您的 eslint 配置中。

module.exports = {
plugins: [
'eslint-plugin-react-compiler',
],
rules: {
'react-compiler/react-compiler': "error",
},
}

eslint 插件会在您的编辑器中显示任何违反 React 规则的行为。当它这样做时,意味着编译器已跳过优化该组件或钩子。这完全没问题,编译器可以恢复并继续优化代码库中的其他组件。

您不必立即修复所有 eslint 违规行为。 您可以按照自己的节奏解决这些问题,以增加正在优化的组件和钩子的数量,但并非必须在使用编译器之前修复所有问题。

将编译器推广到您的代码库

现有项目

该编译器旨在编译遵循 React 规则 的函数组件和钩子。它还可以通过跳过这些组件或钩子来处理违反这些规则的代码。但是,由于 JavaScript 的灵活性,编译器无法捕获所有可能的违规行为,并且可能会在编译时出现假阴性:也就是说,编译器可能会意外地编译违反 React 规则的组件/钩子,从而导致未定义的行为。

因此,为了在现有项目中成功采用编译器,我们建议您先在产品代码中的一个小目录上运行它。您可以通过将编译器配置为仅在特定目录集上运行来实现这一点。

const ReactCompilerConfig = {
sources: (filename) => {
return filename.indexOf('src/path/to/dir') !== -1;
},
};

在极少数情况下,您还可以使用 compilationMode: "annotation" 选项将编译器配置为以“选择加入”模式运行。这使得编译器只会编译使用 "use memo" 指令注释的组件和钩子。请注意,annotation 模式是一种临时模式,旨在帮助早期采用者,我们不打算长期使用 "use memo" 指令。

const ReactCompilerConfig = {
compilationMode: "annotation",
};

// src/app.jsx
export default function App() {
"use memo";
// ...
}

当您对编译器的推出更有信心时,您可以将覆盖范围扩展到其他目录,并将其缓慢推广到整个应用程序。

新项目

如果您正在开始一个新项目,则可以在整个代码库中启用编译器,这是默认行为。

用法

Babel

终端
npm install babel-plugin-react-compiler

编译器包含一个 Babel 插件,您可以在构建管道中使用它来运行编译器。

安装后,请将其添加到您的 Babel 配置中。请注意,编译器在管道中首先 运行至关重要。

// babel.config.js
const ReactCompilerConfig = { /* ... */ };

module.exports = function () {
return {
plugins: [
['babel-plugin-react-compiler', ReactCompilerConfig], // must run first!
// ...
],
};
};

babel-plugin-react-compiler 应该在其他 Babel 插件之前运行,因为编译器需要输入源信息进行可靠的分析。

Vite

如果您使用 Vite,则可以将插件添加到 vite-plugin-react 中。

// vite.config.js
const ReactCompilerConfig = { /* ... */ };

export default defineConfig(() => {
return {
plugins: [
react({
babel: {
plugins: [
["babel-plugin-react-compiler", ReactCompilerConfig],
],
},
}),
],
// ...
};
});

Next.js

Next.js 有一个实验性配置来启用 React 编译器。它会自动确保使用 babel-plugin-react-compiler 设置 Babel。

  • 安装 Next.js canary,它使用 React 19 Release Candidate。
  • 安装 babel-plugin-react-compiler
终端
npm install next@canary babel-plugin-react-compiler

然后在 next.config.js 中配置实验性选项。

// next.config.js
/** @type {import('next').NextConfig} */
const nextConfig = {
experimental: {
reactCompiler: true,
},
};

module.exports = nextConfig;

使用实验性选项可确保在以下环境中支持 React 编译器:

  • 应用路由器
  • 页面路由器
  • Webpack(默认)
  • Turbopack(通过 --turbo 选择加入)

Remix

安装 vite-plugin-babel,并将编译器的 Babel 插件添加到其中。

终端
npm install vite-plugin-babel
// vite.config.js
import babel from "vite-plugin-babel";

const ReactCompilerConfig = { /* ... */ };

export default defineConfig({
plugins: [
remix({ /* ... */}),
babel({
filter: /\.[jt]sx?$/,
babelConfig: {
presets: ["@babel/preset-typescript"], // if you use TypeScript
plugins: [
["babel-plugin-react-compiler", ReactCompilerConfig],
],
},
}),
],
});

Webpack

您可以为 React 编译器创建自己的加载器,如下所示:

const ReactCompilerConfig = { /* ... */ };
const BabelPluginReactCompiler = require('babel-plugin-react-compiler');

function reactCompilerLoader(sourceCode, sourceMap) {
// ...
const result = transformSync(sourceCode, {
// ...
plugins: [
[BabelPluginReactCompiler, ReactCompilerConfig],
],
// ...
});

if (result === null) {
this.callback(
Error(
`Failed to transform "${options.filename}"`
)
);
return;
}

this.callback(
null,
result.code,
result.map === null ? undefined : result.map
);
}

module.exports = reactCompilerLoader;

Expo

请参阅Expo 文档以启用并在 Expo 应用中使用 React 编译器。

Metro (React Native)

React Native 通过 Metro 使用 Babel,因此请参阅与 Babel 一起使用部分以获取安装说明。

Rspack

请参阅Rspack 文档以启用并在 Rspack 应用中使用 React 编译器。

Rsbuild

请参阅Rsbuild 文档以启用并在 Rsbuild 应用中使用 React 编译器。

故障排除

要报告问题,请先在React 编译器 Playground上创建一个最小复现,并将其包含在您的错误报告中。您可以在facebook/react存储库中提交问题。

您也可以通过申请成为 React 编译器工作组成员来提供反馈。有关加入的更多详细信息,请参阅README

(0 , _c) is not a function 错误

如果您使用的不是 React 19 RC 及更高版本,则会发生这种情况。要解决此问题,请先将您的应用升级到 React 19 RC

如果您无法升级到 React 19,您可以尝试工作组中所述的缓存函数的用户空间实现。但是,请注意,不建议这样做,您应该尽可能升级到 React 19。

如何知道我的组件已优化?

React Devtools(v5.0+)内置了对 React 编译器的支持,并在已由编译器优化的组件旁边显示“Memo ✨”徽章。

编译后出现问题

如果您安装了 eslint-plugin-react-compiler,编译器会在您的编辑器中显示任何违反 React 规则的行为。当它这样做时,意味着编译器已跳过优化该组件或钩子。这完全没问题,编译器可以恢复并继续优化代码库中的其他组件。 您不必立即修复所有 eslint 违规行为。 您可以按照自己的节奏解决它们,以增加正在优化的组件和钩子的数量。

然而,由于 JavaScript 的灵活性和动态性,不可能全面检测到所有情况。在这些情况下,可能会发生错误和未定义的行为,例如无限循环。

如果您的应用程序在编译后无法正常工作,并且您没有看到任何 eslint 错误,则编译器可能无法正确编译您的代码。要确认这一点,请尝试通过"use no memo" 指令积极选择退出您认为可能与之相关的任何组件或钩子,从而尝试解决该问题。

function SuspiciousComponent() {
"use no memo"; // opts out this component from being compiled by React Compiler
// ...
}

注意

"use no memo"

"use no memo" 是一个临时的安全出口,允许您选择退出组件和钩子,使其不被 React 编译器编译。此指令并非像例如 "use client" 那样长期存在。

除非绝对必要,否则不建议使用此指令。一旦您选择退出组件或钩子,除非删除该指令,否则它将永远退出。这意味着即使您修复了代码,编译器仍然会跳过编译它,除非您删除该指令。

当您消除错误后,请确认移除退出指令是否会导致问题再次出现。然后使用React 编译器游乐场与我们分享错误报告(您可以尝试将其简化为一个小的可复现示例,或者如果它是开源代码,您也可以直接粘贴整个源代码),以便我们识别并帮助修复问题。

其他问题

请参阅 https://github.com/reactwg/react-compiler/discussions/7