在本教程中,你将学习 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,你将需要有一个自定义的缓存函数实现。你可以查看这个线程 描述的变通方法。