React 编译器

本页面将为您介绍 React 编译器以及如何成功试用它。

建设中

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

你将学习

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

注意

React 编译器是一个新的、目前处于 Beta 阶段的编译器,我们已将其开源以获得社区的早期反馈。虽然它已被 Meta 等公司用于生产环境,但将编译器推广到您应用程序的生产环境取决于您的代码库的健康状况以及您遵循React 规则的程度。

最新的 Beta 版本可以使用带有@beta标签找到,每日实验版本则带有@experimental标签。

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

编译器还包含一个ESLint 插件,该插件可以直接在您的编辑器中显示编译器的分析结果。我们强烈建议每个人都使用这个 linter。Linter 不需要您安装编译器,因此即使您尚未准备好试用编译器,也可以使用它。

编译器当前发布为beta版本,可在 React 17+ 应用程序和库中试用。要安装 Beta 版本:

终端
npm install -D babel-plugin-react-compiler@beta eslint-plugin-react-compiler@beta

或者,如果您使用的是 Yarn:

终端
yarn add -D babel-plugin-react-compiler@beta eslint-plugin-react-compiler@beta

如果您尚未使用 React 19,请参阅下面的部分以获取更多说明。

编译器能做什么?

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

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

注意

React 编译器可以静态地检测到何时违反了 React 规则,并安全地选择退出仅优化受影响的组件或 Hook。编译器无需优化 100% 的代码库。

如果您的代码库已经很好地进行了记忆化,您可能不会期望看到编译器带来重大的性能改进。但是,在实践中,手动记忆导致性能问题的正确依赖项是很棘手的。

深入探讨

React 编译器添加了什么类型的记忆化?

React 编译器的初始版本主要侧重于改进更新性能(重新渲染现有组件),因此它专注于以下两种用例:

  1. 跳过组件的级联重新渲染
    • 重新渲染<Parent />会导致其组件树中的许多组件重新渲染,即使只有<Parent />发生了更改。
  2. 跳过来自 React 外部的昂贵计算
    • 例如,在需要该数据的组件或 Hook 内调用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 在许多不同的组件中使用,即使传递了完全相同的项目,也会重复运行该昂贵的计算。我们建议首先进行 性能分析 ,以查看它是否真的那么昂贵,然后再使代码更复杂。

我应该尝试使用编译器吗?

请注意,编译器仍处于 Beta 阶段,并且存在许多不足之处。虽然它已在 Meta 等公司投入生产使用,但将编译器部署到你的应用程序的生产环境中,将取决于你的代码库的健康状况以及你遵循 React 规则 的程度。

你不必急于现在使用编译器。在它达到稳定版本之前等待使用是可以的。但是,我们确实感谢你在你的应用程序中进行小型实验来尝试它,以便你可以 向我们提供反馈 ,以帮助改进编译器。

入门

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

安装 eslint-plugin-react-compiler

React 编译器还支持一个 ESLint 插件。ESLint 插件可以 独立于编译器使用,这意味着即使你不使用编译器,也可以使用 ESLint 插件。

终端
npm install -D eslint-plugin-react-compiler@beta

然后,将其添加到你的 ESLint 配置中

import reactCompiler from 'eslint-plugin-react-compiler'

export default [
{
plugins: {
'react-compiler': reactCompiler,
},
rules: {
'react-compiler/react-compiler': 'error',
},
},
]

或者,在已弃用的 eslintrc 配置格式中

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;
},
};

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

新项目

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

将 React 编译器与 React 17 或 18 一起使用

React 编译器最适合 React 19 RC。如果您无法升级,您可以安装额外的react-compiler-runtime包,这将允许编译后的代码在 19 之前的版本上运行。但是,请注意,最低支持版本是 17。

终端
npm install react-compiler-runtime@beta

您还应该将正确的target添加到您的编译器配置中,其中target是您目标的 React 主版本。

// babel.config.js
const ReactCompilerConfig = {
target: '18' // '17' | '18' | '19'
};

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

在库中使用编译器

React 编译器也可以用于编译库。因为 React 编译器需要在任何代码转换之前的原始源代码上运行,所以应用程序的构建管道无法编译它们使用的库。因此,我们建议库维护者使用编译器独立编译和测试他们的库,并将编译后的代码发布到 npm。

因为您的代码是预编译的,所以您的库的用户无需启用编译器即可受益于应用于您的库的自动备忘录。如果您的库的目标应用程序尚未使用 React 19,请指定最小target并添加react-compiler-runtime作为直接依赖项。运行时包将根据应用程序的版本使用正确的 API 实现,并在必要时填充缺少的 API。

库代码通常需要更复杂的模式和逃逸门的用法。因此,我们建议确保您进行了充分的测试,以识别使用编译器处理库时可能出现的任何问题。如果您发现任何问题,您始终可以使用'use no memo'指令选择退出特定组件或钩子。

与应用程序类似,无需完全编译 100% 的组件或钩子即可在库中看到好处。一个好的起点可能是确定库中最敏感的性能部分,并确保它们不会破坏React 规则,您可以使用eslint-plugin-react-compiler来识别。

用法

Babel

终端
npm install babel-plugin-react-compiler@beta

编译器包含一个 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 文档了解更多信息。

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

社区提供的Webpack加载器现已在此处提供

Expo

请参考Expo文档,了解如何在Expo应用中启用和使用React编译器。

Metro (React Native)

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

Rspack

请参考Rspack文档,了解如何在Rspack应用中启用和使用React编译器。

Rsbuild

请参考Rsbuild文档,了解如何在Rsbuild应用中启用和使用React编译器。

故障排除

要报告问题,请首先在React Compiler Playground上创建一个最小的可复现示例,并将其包含在您的错误报告中。您可以在facebook/react代码库中提交问题。

您也可以通过申请成为成员来在React Compiler工作组中提供反馈。请查看README以了解更多关于加入的细节

编译器做了哪些假设?

React编译器假设您的代码:

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

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

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

React Devtools (v5.0+) 内置支持React编译器,并将显示“Memo ✨”徽章,以指示哪些组件已由编译器优化。

编译后出现问题 如果你安装了eslint-plugin-react-compiler,编译器会在你的编辑器中显示任何React规则的违规情况。出现这种情况意味着编译器跳过了对该组件或hook的优化。这完全没问题,编译器可以恢复并继续优化代码库中的其他组件。你无需立即修复所有ESLint违规。你可以按照自己的节奏解决它们,以增加被优化的组件和hook的数量。

但是,由于JavaScript的灵活性和动态性,无法全面检测所有情况。在这些情况下,可能会出现错误和未定义的行为,例如无限循环。

如果你的应用在编译后无法正常工作,并且你没有看到任何ESLint错误,则编译器可能正在错误地编译你的代码。为了确认这一点,尝试使用"use no memo" 指令积极地排除你认为可能相关的任何组件或hook,看看问题是否消失。

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

注意

"use no memo"

"use no memo" 是一个临时的应急措施,允许你选择不将组件和hook由React编译器编译。此指令并非像例如"use client"那样具有长期作用。

除非绝对必要,否则不建议使用此指令。一旦你排除了某个组件或hook,它就会永久排除,直到移除该指令。这意味着即使你修复了代码,编译器仍然会跳过编译它,除非你删除该指令。

当你解决错误后,请确认移除排除指令会使问题再次出现。然后,使用React编译器 Playground与我们分享错误报告(你可以尝试将其简化为一个小的重现示例,或者如果它是开源代码,你也可以直接粘贴整个源代码),以便我们能够识别并帮助解决此问题。

其他问题

请查看 https://github.com/reactwg/react-compiler/discussions/7