在本教程中,你将学习 React Compiler如何帮助你编写更优化的 React 应用程序。
React 是一个用户界面库,在过去十多年里一直很好地履行其职责。组件架构、单向数据流和声明性质在帮助开发者构建生产就绪、可扩展的软件应用程序方面脱颖而出。
在发布(甚至直到最新的稳定版本 v18.x)中,React 提供了各种技术和方法来提高应用程序性能。
例如,通过使用 React.memo() 高阶组件,或者像 useMemo() 和 useCallback() 这样的钩子,整个记忆化范式得到了支持。
在编程中,记忆化 是一种优化技术,它通过缓存昂贵计算的结果使程序执行得更快。
尽管 React 的 记忆化 技术对于应用优化非常有用,正如本叔叔(记得吗,蜘蛛侠的叔叔?)曾经说过的,“能力越大,责任越大”。所以我们作为开发者在应用它们时需要更加负责。优化很好,但过度优化可能会扼杀应用程序的性能。
随着 React 19 的发布,开发者社区收到了一系列增强功能和特性:
一个实验性的开源编译器。我们将主要关注本文中的它。
React 服务器组件。
服务器操作。
处理文档元数据的更简单、更自然的方式。
增强的钩子和 API。
可以将
ref作为属性传递。样式、图像和字体的资产加载改进。
与 Web 组件的更平滑集成。
如果这些让你感到兴奋,我建议你观看这个视频,它解释了每个特性将如何影响你作为 React 开发者。我希望你喜欢它😊。
React 19 引入的 编译器 将是改变游戏规则的。从现在开始,我们可以让编译器处理优化的头痛问题,而不是让我们自己承担。
这是否意味着我们不再需要使用 memo、useMemo()、useCallback 等了?不 - 我们大多数情况下不需要。如果你理解和遵循 React 组件和钩子的规则,编译器可以自动处理这些事情。
它将如何做到这一点?好吧,我们会讲到的。但在此之前,让我们先了解什么是 编译器,以及将这个新的 React 代码优化器称为 React Compiler 是否合理。
传统上什么是编译器?
简单来说,编译器是一个软件程序/工具,它将高级编程语言代码(源代码)翻译成机器代码。编译源代码并生成机器代码需要遵循几个步骤:
词法分析器将源代码标记化并生成标记。语法分析器创建一个抽象语法树 (AST) 以逻辑结构化源代码标记。语义分析器验证代码的语义(或句法)正确性。经过上述三种分析器的分析后,会生成一些
中间代码。它也被称为 IR 代码。然后对 IR 代码进行
优化。最后,编译器从优化后的 IR 代码生成
机器代码。

现在你了解了编译器的基本原理,让我们学习 React Compiler 并了解它的工作原理。
React Compiler架构
React Compiler是一个构建时工具,你需要使用 React 工具生态系统提供的配置选项明确配置它与你的 React 19 项目。
例如,如果你使用 Vite 创建你的 React 应用程序,编译器配置将在 vite.config.js 文件中进行。
React Compiler有三个主要组件:
Babel 插件: 在编译过程中帮助转换代码**。ESLint 插件: 帮助捕获和报告任何违反 React 规则的行为。编译器核心:执行代码分析和优化的核心编译器逻辑。Babel 和 ESLint 插件都使用核心编译器逻辑。
编译流程如下:
Babel 插件确定要编译哪些函数(组件或钩子)。我们稍后将看到一些配置,以了解如何选择加入和退出编译过程。插件为每个函数调用核心编译器逻辑,并最终创建抽象语法树。然后编译器核心将 Babel AST 转换为 IR 代码,对其进行分析,并运行各种验证以确保没有违反任何规则。
接下来,它尝试通过执行各种传递来减少要优化的代码量,以消除死代码。使用记忆化进一步优化代码。
最后,在代码生成阶段,转换后的 AST 被转换回优化后的 JavaScript 代码。
React Compiler在行动
现在你已经知道 React Compiler的工作原理,让我们现在深入配置它与 React 19 项目,以便你可以开始了解各种优化。
理解问题:没有 React Compiler
让我们用 React 创建一个简单的产品页面。产品页面显示带有页面上产品数量的标题、产品列表和特色产品。

组件层次结构和组件之间的数据传递如下所示:

正如你在上面的图片中看到的,
ProductPage组件有三个子组件,Heading、ProductList和FeaturedProducts。ProductPage组件接收两个属性,products和heading。ProductPage组件计算产品总数,并将该值连同标题文本值一起传递给Heading组件。ProductPage组件将products属性传递给子组件ProductList。同样,它计算特色产品并将
featuredProducts属性传递给子组件FeaturedProducts。
以下是 ProductPage 组件的源代码可能的样子:
import React from 'react'
import Heading from './Heading';
import FeaturedProducts from './FeaturedProducts';
import ProductList from './ProductList';
const ProductPage = ({products, heading}) => {
const featuredProducts = products.filter(product => product.featured);
const totalProducts = products.length;
return (
<div className="m-2">
<Heading
heading={heading}
totalProducts={totalProducts} />
<ProductList
products={products} />
<FeaturedProducts
featuredProducts={featuredProducts} />
</div>
)
}
export default ProductPage
同时,假设我们在 App.js 文件中像这样使用 ProductPage 组件:
import ProductPage from "./components/compiler/ProductPage";
function App() {
// 食品产品列表
const foodProducts = [
{
"id": "001",
"name": "Hamburger",
"image": "🍔",
"featured": true
},
{
"id": "002",
"name": "French Fries",
"image": "🍟",
"featured": false
},
{
"id": "003",
"name": "Taco",
"image": "🌮",
"featured": false
},
{
"id": "004",
"name": "Hot Dog",
"image": "🌭",
"featured": true
}
];
return (
<ProductPage
products={foodProducts}
heading="The Food Product" />
);
}
export default App;
这都很好 - 那么问题在哪里呢?问题是 React 在父组件重新渲染时会主动重新渲染子组件。不必要的渲染需要优化。让我们先充分理解问题。
我们将在每个子组件中添加当前时间戳。现在渲染的用户界面将如下所示:

你看到的大数字旁边的标题是时间戳(使用 JavaScript Date API 中的简单 Date.now() 函数)我们已经添加到组件代码中。现在,如果我们改变 ProductPage 组件的 heading 属性的值会怎样?
之前:
<ProductPage
products={foodProducts}
heading="The Food Product" />
之后(注意我们已经通过在 heading 值的末尾添加一个 s 使其复数):
<ProductPage
products={foodProducts}
heading="The Food Products" />
现在你会注意到用户界面立即发生了变化。所有三个时间戳都更新了。这是因为当父组件由于属性更改而重新渲染时,所有三个组件都被重新渲染了。

如果你注意到,heading 属性只传递给了 Heading 组件,即使这样,其他两个子组件也被重新渲染了。这就是我们需要优化的地方。
解决问题:没有 React Compiler
如前所述,React 为我们提供了各种钩子和 API 进行 记忆化。我们可以使用 React.memo() 或 useMemo() 来保护不必要重新渲染的组件。
例如,我们可以使用 React.memo() 来记忆化 ProductList 组件,以确保除非 products 属性发生变化,否则 ProductList 组件不会被重新渲染。
我们可以使用 useMemo() 钩子来记忆化特色产品的计算。两种实现在下面的图片中指出。

但是,再次回忆一下伟大的本叔叔的明智话语,在过去的几年里,我们开始过度使用这些优化技术。这些过度优化可能会对应用程序的性能产生负面影响。因此,编译器的可用性对 React 开发者来说是一个福音,因为它让他们将许多这样的优化委托给编译器。
现在让我们使用 React Compiler来解决问题。
解决问题:使用 React Compiler
再次,React Compiler是一个可选择的构建时工具。它不包含在 React 19 RC 中。你需要安装所需的依赖项,并使用 React 19 项目配置编译器。
在配置编译器之前,你可以通过在项目目录中执行此命令来检查你的代码库是否兼容:
npx react-compiler-healthcheck@experimental
它将检查并报告:
编译器可以优化多少组件
是否遵循了 React 规则。
是否有任何不兼容的库。

如果你发现事情是兼容的,是时候安装由 React Compiler支持的 ESLint 插件了。这个插件将帮助你捕获代码中任何违反 React 规则的行为。违反代码将被 React Compiler跳过,不会对其执行任何优化。
npm install eslint-plugin-react-compiler@experimental
然后打开 ESLint 配置文件(例如,对于 Vite,是 .eslintrc.cjs)并添加这些配置:
module.exports = {
plugins: [
'eslint-plugin-react-compiler',
],
rules: {
'react-compiler/react-compiler': "error",
},
}
接下来,你将使用 React Compiler的 Babel 插件来为你的整个项目启用编译器。如果你正在使用 React 19 开始一个新项目,我建议你为整个项目启用 React Compiler。让我们安装 React Compiler的 Babel 插件:
npm install babel-plugin-react-compiler@experimental
安装完成后,你需要通过在 Babel 配置文件中添加选项来完成配置。由于我们使用的是 Vite,打开 vite.config.js 文件并将内容替换为以下代码片段:
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
const ReactCompilerConfig = {/* ... */ };
// https://vitejs.dev/config/
export default defineConfig({
plugins: [react({
babel: {
plugins: [
[
"babel-plugin-react-compiler",
ReactCompilerConfig
]
],
},
})],
})
在这里,你已经将 babel-plugin-react-compiler 添加到了配置中。ReactCompilerConfig 需要提供任何高级配置,例如如果你想提供任何自定义运行时模块或其他配置。在这种情况下,它是一个没有任何高级配置的空对象。
就这样。你已经完成了 React Compiler与你的代码库的配置,以利用它的力量。从现在开始,React Compiler将查看你项目中的每个组件和钩子,并尝试对其进行优化。
如果你想将 React Compiler与 Next.js、Remix、Webpack 等配置,你可以遵循这个指南。
使用 React Compiler优化的 React 应用程序
现在你应该有一个包含 React Compiler的优化 React 应用程序。所以,让我们再次运行你之前做过的测试。再次改变 ProductPage 组件的 heading 属性的值。
这次,你将不会看到子组件重新渲染。所以时间戳也不会更新。但你将看到组件中数据发生变化的部分,因为它将单独反映变化。而且,你再也不必在代码中使用 memo、useMemo() 或 useCallback() 了。
你可以在这里直观地看到它的工作。
React DevTools 中的 React Compiler
React DevTools 5.0+ 版本内置了对 React Compiler的支持。你将在 React Compiler优化的组件旁边看到一个带有文本 Memo ✨ 的徽章。这太棒了!

深入了解 - React Compiler是如何工作的?
现在你已经看到了 React Compiler在 React 19 代码上的工作方式,让我们深入理解后台发生了什么。我们将使用 React 编译器游乐场 来探索翻译后的代码和优化步骤。

我们将使用 Heading 组件作为示例。将以下代码复制并粘贴到游乐场最左侧的部分:
const Heading = ({ heading, totalProducts }) => {
return (
<nav>
<h1 className="text-2xl">
{heading}({totalProducts}) - {Date.now()}
</h1>
</nav>
)
}
你会看到一些 JavaScript 代码立即在游乐场的 _JS 标签内生成。React Compiler作为编译过程的一部分生成了这个 JavaScript 代码。让我们一步一步地进行:
function anonymous_0(t0) {
const $ = _c(4);
const { heading, totalProducts } = t0;
let t1;
if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
t1 = Date.now();
$[0] = t1;
} else {
t1 = $[0];
}
let t2;
if ($[1] !== heading || $[2] !== totalProducts) {
t2 = (
<nav>
<h1 className="text-2xl">
{heading}({totalProducts}) - {t1}
</h1>
</nav>
);
$[1] = heading;
$[2] = totalProducts;
$[3] = t2;
} else {
t2 = $[3];
}
return t2;
}
编译器使用一个名为 _c() 的钩子来创建一个要缓存的项目数组。在上面的代码中,创建了一个包含四个元素的数组来缓存四个项目。
const $ = _c(4);
但是,要缓存的是什么呢?
组件接收两个属性,
heading和totalProducts。编译器需要缓存它们。因此,它需要缓存项目数组中的两个元素。标题中的
Date.now()部分应该被缓存。JSX 本身应该被缓存。除非上述任何一个变化,否则没有必要计算 JSX。
所以总共有四个项目要缓存。
编译器使用 if-block 创建记忆化块。编译器的最终返回值是 JSX,它依赖于三个依赖项:
Date.now()值。两个属性,一个
heading和totalProducts
当上述任何一个变化时,输出 JSX 需要重新计算。这意味着编译器需要为上述每一个创建两个记忆化块。
第一个记忆化块看起来像这样:
if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
t1 = Date.now();
$[0] = t1;
} else {
t1 = $[0];
}
if-block 将 Date.now() 的值存储在可缓存数组的第一个索引中。它每次都重用相同的,除非它改变了。
同样,在第二个记忆化块中:
if ($[1] !== heading || $[2] !== totalProducts) {
t2 = (
<nav>
<h1 className="text-2xl">
{heading}({totalProducts}) - {t1}
</h1>
</nav>
);
$[1] = heading;
$[2] = totalProducts;
$[3] = t2;
} else {
t2 = $[3];
}
在这里,检查是 heading 或 totalProducts 属性的值是否发生变化。如果这些中的任何一个发生变化,就需要重新计算 JSX。然后所有值都存储在可缓存的数组中。如果没有值的变化,就从缓存中返回之前计算的 JSX。
你现在可以将任何其他组件源代码粘贴到左侧,并查看生成的 JavaScript 代码,以帮助您理解编译器在编译过程中执行的记忆化技术,就像我们上面所做的那样。这将帮助你更好地掌握编译器如何执行记忆化技术。
你如何选择加入和退出 React Compiler?
一旦你像我们在 Vite 项目中所做的那样配置了 React Compiler,它就会为我们的项目中的所有编译器和钩子启用。
但在某些情况下,你可能想要选择性地加入 React Compiler。在这种情况下,你可以使用 compilationMode: "annotation" 选项运行编译器的 “选择加入” 模式。
// 在 ReactCompilerConfig 中指定选项
const ReactCompilerConfig = {
compilationMode: "annotation",
};
然后使用 "use memo" 指令注释你想要选择加入编译的组件和钩子。
// src/ProductPage.jsx
export default function ProductPage() {
"use memo";
// ...
}
请注意,还有一个 "use no memo" 指令。可能有一些罕见的情况,你的组件在编译后可能不会按预期工作,你想暂时退出编译,直到问题被识别和修复。在这种情况下,你可以使用这个指令:
function AComponent() {
"use no memo";
// ...
}
我们可以使用 React 18.x 中的 React Compiler吗?
建议与 React 19 一起使用 React Compiler,因为需要兼容。如果你不能将你的应用程序升级到 React 19,你将需要有一个自定义的缓存函数实现。你可以查看这个线程 描述的变通方法。
