在本教程中,您将了解 React 编译器如何帮助您编写更优化的 React 应用程序。
React 是一个用户界面库,十多年来一直表现不俗。其组件架构、单向数据流和声明性在帮助开发人员构建可用于生产且可扩展的软件应用程序方面脱颖而出。
在各个版本中(甚至直到最新的稳定版本 v18.x),React 提供了各种技术和方法来提高应用程序的性能。
例如,整个记忆范式已经通过React.memo()
高阶组件或像useMemo()
和 这样的钩子得到支持useCallback()
。
在编程中,memoization
是一种优化技术,通过缓存昂贵的计算结果来使程序执行得更快。
尽管 React 的memoization
技术非常适合应用优化,但正如 Uncle Ben(还记得蜘蛛侠的叔叔吗?)曾经说过的,"能力越大,责任越大"。因此,作为开发人员,我们在应用这些优化时需要更负责任一些。优化很棒,但过度优化可能会损害应用程序的性能。
随着 React 19 的推出,开发者社区获得了一系列值得夸耀的增强功能和特性:
- 一个实验性的开源编译器。本文将主要关注它。
- React 服务器组件。
- 服务器操作。
- 处理文档元数据的更简单、更有机的方式。
- 增强的钩子和 API。
ref
可以作为道具传递。- 样式、图像和字体的资产加载得到改进。
- 与 Web 组件的集成更加顺畅。
compiler
with 的引入React 19
必将改变游戏规则。从现在起,我们可以让编译器处理优化难题,而不是把它留给我们。
这是否意味着我们不再需要使用memo
、、等等?不,我们基本上不需要。如果您理解并遵循React 的组件和钩子规则useMemo()
,编译器可以自动处理这些事情。useCallback
它将如何做到这一点?好吧,我们马上开始。但在此之前,让我们先了解一下什么compiler
是 a ,以及将这个新的 React 代码优化器称为 是否合理React Compiler
。
传统上,什么是编译器?
简单来说,编译器是一种将高级编程语言代码(源代码)翻译成机器码的软件程序/工具。编译源代码并生成机器码需要遵循以下几个步骤:
- 对
lexical analyzer
源代码进行标记并生成标记。 - 创建
Syntax Analyzer
一个抽象语法树 (AST) 来逻辑地构造源代码标记。 - 验证
Semantic Analyzer
代码的语义(或句法)正确性。 - 这三种分析方法分别经过相应的分析器分析后,
intermediate code
会生成一些代码,也称为 IR 代码。 - 然后
optimization
对 IR 代码执行。 - 最后,
machine code
由编译器根据优化后的 IR 代码生成。
现在您已经了解了编译器工作原理的基础知识,让我们来学习React Compiler
并了解其工作原理。
React 编译器架构
React 编译器是一个构建时工具,您需要使用 React 工具生态系统提供的配置选项明确配置您的 React 19 项目。
例如,如果您使用Vite
创建 React 应用程序,则编译器配置将在文件中进行vite.config.js
。
React 编译器有三个主要组件:
Babel Plugin
:帮助在编译过程中转换代码。ESLint Plugin
:帮助捕获和报告任何违反 React 规则的行为。Compiler Core
:执行代码分析和优化的核心编译器逻辑。Babel 和 ESLint 插件都使用核心编译器逻辑。
编译流程如下:
- 标识
Babel Plugin
要编译哪些函数(组件或钩子)。稍后我们将看到一些配置,以了解如何选择加入和退出编译过程。该插件为每个函数调用核心编译器逻辑,最终创建抽象语法树。 - 然后,编译器核心将 Babel AST 转换为 IR 代码,对其进行分析,并运行各种验证以确保没有违反任何规则。
- 接下来,它会尝试通过执行各种步骤来消除死代码,从而减少需要优化的代码量。使用记忆化功能可以进一步优化代码。
- 最后,在代码生成阶段,将转换后的 AST 转换回优化的 JavaScript 代码。
React Compiler 实际应用
现在您已经了解了 React Compiler 的工作原理,现在让我们深入研究使用 React 19 项目对其进行配置,以便您可以开始了解各种优化。
理解问题:没有 React Compiler
让我们用 React 创建一个简单的产品页面。产品页面显示一个标题,其中包含页面上的产品数量、产品列表和特色产品。
组件层次结构和组件之间传递的数据如下所示:
正如你在上图中所看到的,
- 该
ProductPage
组件有三个子组件,Heading
、ProductList
和FeaturedProducts
。 - 该
ProductPage
组件接收两个 props,products
以及heading
。 - 该
ProductPage
组件计算产品总数并将该值连同标题文本值一起传递给Heading
组件。 - 该
ProductPage
组件将 prop 传递products
给ProductList
子组件。 - 类似地,它计算特色产品并将
featuredProducts
prop 传递给FeaturedProducts
子组件。
该组件的源代码可能如下所示ProductPage
:
javascript
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;
另外,假设我们ProductPage
在App.js
文件中像这样使用该组件:
javascript
import ProductPage from './components/compiler/ProductPage';
function App() {
// A list of food products
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 会主动重新渲染子组件。不必要的渲染需要优化。让我们首先充分了解问题所在。
我们将在每个子组件中添加当前时间戳。现在呈现的用户界面将如下所示:
标题旁边的大数字是Date.now()
我们添加到组件代码中的时间戳(使用 JavaScript Date API 中的简单函数)。现在,如果我们更改组件的标题 prop 的值,会发生什么ProductPage
?
前:
xml
<ProductPage
products={foodProducts}
heading="The Food Product" />
后:(请注意,我们在值的末尾添加了一个s
,使产品变为复数heading
):
xml
<ProductPage
products={foodProducts}
heading="The Food Products" />
现在你会注意到用户界面立即发生了变化。所有三个时间戳都已更新。这是因为当父组件由于 props 更改而重新渲染时,所有三个组件都重新渲染了。
如果你注意到,heading
prop 只传递给了Heading
组件,但其他两个子组件也重新渲染了。这就是我们需要优化的地方。
修复问题:没有 React 编译器
如前所述,React 为我们提供了各种钩子和 API memoization
。我们可以使用React.memo()
或useMemo()
来保护那些不必要重新渲染的组件。
例如,我们可以用来React.memo()
记忆 ProductList 组件,以确保除非products
prop 发生变化,否则该ProductList
组件不会重新渲染。
我们可以使用useMemo()
钩子来记忆特色产品的计算。下图显示了两种实现方式。
但是,再次回想一下伟大的本叔叔的明智之言,在过去的几年中,我们已经开始过度使用这些优化技术。这些过度优化会对应用程序的性能产生负面影响。因此,编译器的可用性对 React 开发人员来说是一个福音,因为它允许他们将许多此类优化委托给编译器。
现在让我们使用 React 编译器来解决这个问题。
修复问题:使用 React Compiler
再次强调,React 编译器是一个可选的构建时工具。它不与 React 19 RC 捆绑在一起。您需要安装所需的依赖项并使用您的 React 19 项目配置编译器。
在配置编译器之前,您可以通过在项目目录上执行此命令来检查代码库是否兼容:
bash
npx react-compiler-healthcheck@experimental
它将检查并报告:
- 编译器可以优化多少个组件
- 如果遵循 React 规则。
- 如果有任何不兼容的库。
如果你发现兼容,那么是时候安装由 React 编译器支持的 ESLint 插件了。此插件将帮助你捕获代码中任何违反 React 规则的代码。违反规则的代码将被 React 编译器跳过,并且不会对其进行任何优化。
bash
npm install eslint-plugin-react-compiler@experimental
然后打开 ESLint 配置文件(例如,.eslintrc.cjs
对于 Vite)并添加以下配置:
javascript
module.exports = {
plugins: ['eslint-plugin-react-compiler'],
rules: {
'react-compiler/react-compiler': 'error',
},
};
接下来,您将使用 React 编译器的 Babel 插件为整个项目启用编译器。如果您使用 React 19 开始新项目,我建议您为整个项目启用 React 编译器。让我们安装 React 编译器的 Babel 插件:
bash
npm install babel-plugin-react-compiler@experimental
安装完成后,您需要通过在 Babel 配置文件中添加选项来完成配置。由于我们使用的是 Vite,请打开文件vite.config.js
并使用以下代码片段替换内容:
javascript
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 编译器,以利用其功能。从现在开始,React 编译器将检查项目中的每个组件和钩子,尝试对其进行优化。
如果你想使用 Next.js、Remix、Webpack 等配置 React 编译器,你可以遵循本指南。
使用 React Compiler 优化 React 应用
现在,您应该拥有一个包含 React 编译器的优化 React 应用。那么,让我们运行之前执行过的相同测试。再次更改组件heading
prop 的值ProductPage
。
这次,您将不会看到子组件重新渲染。因此时间戳也不会更新。但您将看到组件中数据发生变化的部分,因为它将单独反映更改。此外,您不必再在代码中使用memo
、useMemo()
或。useCallback()
您可以从这里直观地看到它的工作原理。
React DevTools 中的 React Compiler
React DevTools 5.0+ 版本内置了对 React 编译器的支持。您将Memo ✨
在 React 编译器优化的组件旁边看到一个带有文本的徽章。这太棒了!
深入探究 -- React 编译器如何工作?
现在您已经了解了 React 编译器如何处理 React 19 代码,让我们深入了解后台发生的情况。我们将使用 React Compiler Playground来探索翻译后的代码和优化步骤。
我们将使用该Heading
组件作为示例。将以下代码复制并粘贴到 Playground 最左侧部分:
javascript
const Heading = ({ heading, totalProducts }) => {
return (
<nav>
<h1 className='text-2xl'>
{heading}({totalProducts}) - {Date.now()}
</h1>
</nav>
);
};
您将看到在 Playground 的选项卡内立即生成了一些 JavaScript 代码_JS
。React 编译器在编译过程中生成了此 JavaScript 代码。让我们一步一步地介绍一下:
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()
创建要缓存的项目数组。在上面的代码中,创建了一个包含四个元素的数组来缓存四个项目。
javascript
const $ = _c(4);
但是,需要缓存什么东西呢?
- 该组件接受两个 props,
heading
和totalProducts
。编译器需要缓存它们。因此,它需要可缓存项数组中的两个元素。 Date.now()
标头中的部分应该被缓存。- JSX 本身应该被缓存。除非上述任何一项发生变化,否则计算 JSX 毫无意义。
因此总共有四个项目需要缓存。
编译器使用 创建记忆块if-block
。编译器的最终返回值是 JSX,它依赖于三个依赖项:
- 值
Date.now()
。 - 两个道具,一个
heading
和totalProducts
当上述任何一项发生更改时,输出的 JSX 都需要重新计算。这意味着编译器需要为上述每一项创建两个记忆块。
第一个记忆块如下所示:
javascript
if ($[0] === Symbol.for('react.memo_cache_sentinel')) {
t1 = Date.now();
$[0] = t1;
} else {
t1 = $[0];
}
if 块将 Date.now() 的值存储到可缓存数组的第一个索引中。除非发生更改,否则每次都会重复使用相同的值。
类似地,在第二个记忆块中:
javascript
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
props 的值是否发生变化。如果其中任何一个发生变化,则需要重新计算 JSX。然后所有值都存储在可缓存数组中。如果值没有变化,则从缓存中返回之前计算的 JSX。
现在,您可以将任何其他组件源代码粘贴到左侧,并查看生成的 JavaScript 代码,以帮助您了解上面所做的操作。这将帮助您更好地了解编译器在编译过程中如何执行记忆技术。
如何加入或退出 React 编译器?
一旦您按照我们在此处对 Vite 项目所做的方式配置了 React 编译器,它就会为项目的所有编译器和钩子启用。
但在某些情况下,你可能希望有选择地启用 React 编译器。在这种情况下,你可以使用选项以"启用"模式运行编译器compilationMode: "annotation"
。
javascript
// Specify the option in the ReactCompilerConfig
const ReactCompilerConfig = {
compilationMode: 'annotation',
};
然后使用该指令注释您想要选择加入编译的组件和钩子"use memo"
。
javascript
// src/ProductPage.jsx
export default function ProductPage() {
'use memo';
// ...
}
请注意,还有一个"use no memo"
指令。在极少数情况下,您的组件在编译后可能无法按预期工作,并且您希望暂时退出编译,直到问题被识别并修复。在这种情况下,您可以使用此指令:
javascript
function AComponent() {
'use no memo';
// ...
}
我们可以将 React 编译器与 React 18.x 一起使用吗?
建议将 React 编译器与 React 19 一起使用,因为需要兼容性。如果您无法将应用程序升级到 React 19,则需要自定义缓存功能。