高效的 React 组件:优化 React 性能的指南

已发表: 2022-03-11

自推出以来,React 改变了前端开发人员构建 Web 应用程序的方式。 借助虚拟 DOM,React 使 UI 更新尽可能高效,让您的 Web 应用程序更快捷。 但是,为什么中等大小的 React Web 应用程序仍然表现不佳?

好吧,线索在于你如何使用 React。

像 React 这样的现代前端库不会神奇地让你的应用程序更快。 它要求开发人员了解 React 的工作原理以及组件如何在组件生命周期的各个阶段中生存。

使用 React,您可以通过测量和优化组件的渲染方式和时间来获得它必须提供的大量性能改进。 而且,React 只提供了使这一切变得容易所需的工具和功能。

通过优化组件的渲染差异过程来加速你的 React 应用程序。

在本 React 教程中,您将学习如何测量 React 组件的性能并优化它们以构建性能更高的 React Web 应用程序。 您还将了解一些 JavaScript 最佳实践如何帮助您的 React Web 应用程序提供更流畅的用户体验。

React 是如何工作的?

在深入研究优化技术之前,我们需要更好地了解 React 的工作原理。

在 React 开发的核​​心,您拥有简单明了的 JSX 语法,以及 React 构建和比较虚拟 DOM 的能力。 自发布以来,React 影响了许多其他前端库。 Vue.js 等库也依赖于虚拟 DOM 的概念。

以下是 React 的工作原理:

每个 React 应用程序都从一个根组件开始,并由许多组件组成的树形结构。 React 中的组件是基于它接收到的数据(props 和 state)呈现 UI 的“函数”。

我们可以将其表示为F

 UI = F(data)

用户与 UI 交互并导致数据发生变化。 无论交互是否涉及单击按钮、点击图像、拖动列表项、调用 API 的 AJAX 请求等,所有这些交互都只会更改数据。 它们永远不会导致 UI 直接更改。

在这里,数据是定义 Web 应用程序状态的所有内容,而不仅仅是您存储在数据库中的内容。 甚至前端状态的位(例如,当前选择了哪个选项卡或当前是否选中了复选框)也是该数据的一部分。

每当这些数据发生变化时,React 都会使用组件函数重新渲染 UI,但只是虚拟的:

 UI1 = F(data1) UI2 = F(data2)

React 通过在其虚拟 DOM 的两个版本上应用比较算法来计算当前 UI 和新 UI 之间的差异。

 Changes = Diff(UI1, UI2)

然后 React 继续仅将 UI 更改应用到浏览器上的真实 UI。

当与组件关联的数据发生变化时,React 会确定是否需要进行实际的 DOM 更新。 这允许 React 避免在浏览器中进行潜在的昂贵的 DOM 操作操作,例如创建 DOM 节点和不必要地访问现有节点。

这种重复的组件差异和渲染可能是任何 React 应用程序中 React 性能问题的主要来源之一。 构建一个 React 应用程序,其中 diffing 算法无法有效协调,导致整个应用程序重复呈现,这可能会导致令人沮丧的缓慢体验。

从哪里开始优化?

但我们优化的究竟是什么?

你看,在初始渲染过程中,React 构建了一个 DOM 树,如下所示:

React 组件的虚拟 DOM

给定部分数据更改,我们希望 React 只重新渲染直接受更改影响的组件(甚至可能跳过其余组件的 diff 过程):

React 渲染最佳数量的组件

然而,React 最终做的是:

反应浪费资源渲染所有组件

在上图中,所有黄色节点都被渲染和比较,导致时间/计算资源的浪费。 这是我们将主要进行优化工作的地方。将每个组件配置为仅在必要时渲染差异将使我们能够回收这些浪费的 CPU 周期。

React 库的开发人员考虑到了这一点,并为我们提供了一个钩子来做到这一点:一个让我们告诉 React 什么时候可以跳过渲染组件的函数。

先测量

正如 Rob Pike 将其作为他的编程规则之一相当优雅地指出的那样:

措施。 在您测量之前不要调整速度,即使这样也不要调整,除非代码的一部分压倒了其余部分。

不要开始优化您认为可能会降低应用程序速度的代码。 相反,让 React 性能测量工具引导您完成整个过程。

React 有一个强大的工具来解决这个问题。 使用react-addons-perf库,您可以了解应用程序的整体性能。

用法很简单:

 Import Perf from 'react-addons-perf' Perf.start(); // use the app Perf.stop(); Perf.printWasted();

这将打印一个表格,其中包含在渲染中浪费的组件时间量。

在渲染中浪费时间的组件表

该库提供了其他函数,可以让您分别打印浪费时间的不同方面(例如,使用printInclusive()printExclusive()函数),甚至打印 DOM 操作操作(使用printOperations()函数)。

让基准测试更进一步

如果你是一个视觉型的人,那么react-perf-tool正是你所需要的。

react-perf-tool基于react-addons-perf库。 它为您提供了一种更直观的方式来调试 React 应用程序的性能。 它使用底层库来获取测量结果,然后将它们可视化为图形。

组件在渲染中浪费时间的可视化

通常,这是一种更方便的发现瓶颈的方法。 您可以通过将其作为组件添加到您的应用程序来轻松使用它。

React 应该更新组件吗?

默认情况下,React 将运行,渲染虚拟 DOM,并比较树中每个组件的差异,以了解其 props 或 state 的任何变化。 但这显然是不合理的。

随着您的应用程序的增长,尝试在每个操作处重新渲染和比较整个虚拟 DOM 最终会减慢速度。

React 为开发人员提供了一种简单的方法来指示组件是否需要重新渲染。 这就是shouldComponentUpdate方法发挥作用的地方。

 function shouldComponentUpdate(nextProps, nextState) { return true; }

当此函数为任何组件返回 true 时,它​​允许触发 render-diff 过程。

这为您提供了一种控制渲染差异过程的简单方法。 每当您需要防止组件被重新渲染时,只需从函数中返回false即可。 在函数内部,您可以比较当前和下一组 props 和 state 以确定是否需要重新渲染:

 function shouldComponentUpdate(nextProps, nextState) { return nextProps.id !== this.props.id; }

使用 React.PureComponent

为了简化和自动化这种优化技术,React 提供了所谓的“纯”组件。 React.PureComponent完全一样,它实现了一个React.Component shouldComponentUpdate()函数,具有浅的 prop 和状态比较。

React.PureComponent或多或少等同于这个:

 class MyComponent extends React.Component { shouldComponentUpdate(nextProps, nextState) { return shallowCompare(this.props, nextProps) && shallowCompare(this.state, nextState); } … }

由于它只执行浅比较,您可能会发现它仅在以下情况下才有用:

  • 您的道具或状态包含原始数据。
  • 你的 props 和 states 有复杂的数据,但是你知道什么时候调用forceUpdate()来更新你的组件。

使数据不可变

如果您可以使用React.PureComponent但仍然有一种有效的方式来判断任何复杂的道具或状态何时自动更改,该怎么办? 这就是不可变数据结构让生活更轻松的地方。

使用不可变数据结构背后的想法很简单。 每当包含复杂数据的对象发生更改时,而不是在该对象中进行更改,而是创建具有更改的该对象的副本。 这使得检测数据变化就像比较两个对象的引用一样简单。

您可以使用Object.assign_.extend (来自 Underscore.js 或 Lodash):

 const newValue2 = Object.assign({}, oldValue); const newValue2 = _.extend({}, oldValue);

更好的是,您可以使用提供不可变数据结构的库:

 var map1 = Immutable.Map({a:1, b:2, c:3}); var map2 = map1.set('b', 2); assert(map1.equals(map2) === true); var map3 = map1.set('b', 50); assert(map1.equals(map3) === false);

这里, Immutable.Map由库 Immutable.js 提供。

每次使用其方法set更新映射时,仅当 set 操作更改基础值时才​​会返回新映射。 否则,返回相同的地图。

您可以在此处了解有关使用不可变数据结构的更多信息。

更多 React 应用优化技术

使用生产版本

在开发 React 应用程序时,您会看到非常有用的警告和错误消息。 这些使得在开发过程中识别错误和问题成为一种幸福。 但它们是以性能为代价的。

如果你查看 React 的源代码,你会看到很多if (process.env.NODE_ENV != 'production')检查。 React 在您的开发环境中运行的这些代码块并不是最终用户需要的。 对于生产环境,所有这些不必要的代码都可以丢弃。

如果您使用create-react-app引导您的项目,那么您可以简单地运行npm run build来生成生产版本,而无需这些额外代码。 如果你直接使用 Webpack,你可以运行webpack -p (相当于webpack --optimize-minimize --define process.env.NODE_ENV="'production'"

早期绑定函数

在渲染函数中看到绑定到组件上下文的函数是很常见的。 当我们使用这些函数来处理子组件的事件时,经常会出现这种情况。

 // Creates a new `handleUpload` function during each render() <TopBar onUpload={this.handleUpload.bind(this)} /> // ...as do inlined arrow functions <TopBar onUpload={files => this.handleUpload(files)} />

这将导致render()函数在每个渲染上创建一个新函数。 一个更好的方法是:

 class App extends React.Component { constructor(props) { super(props); this.handleUpload = this.handleUpload.bind(this); } render() { … <TopBar onUpload={this.handleUpload} /> … } }

使用多个块文件

对于单页 React Web 应用程序,我们通常最终将所有前端 JavaScript 代码捆绑在一个压缩文件中。 这适用于中小型 Web 应用程序。 但随着应用程序开始增长,将这个捆绑的 JavaScript 文件传送到浏览器本身可能会成为一个耗时的过程。

如果您使用 Webpack 构建您的 React 应用程序,您可以利用其代码拆分功能将您构建的应用程序代码分成多个“块”,并根据需要将它们交付给浏览器。

拆分有两种类型:资源拆分和按需代码拆分。

通过资源拆分,您可以将资源内容拆分为多个文件。 例如,使用 CommonsChunkPlugin,您可以将公共代码(例如所有外部库)提取到自己的“块”文件中。 使用 ExtractTextWebpackPlugin,您可以将所有 CSS 代码提取到单独的 CSS 文件中。

这种分裂将在两个方面有所帮助。 它可以帮助浏览器缓存那些不经常更改的资源。 它还将帮助浏览器利用并行下载来潜在地减少加载时间。

Webpack 一个更显着的特性是按需代码拆分。 您可以使用它将代码拆分为可以按需加载的块。 这可以使初始下载保持较小,从而减少加载应用程序所需的时间。 然后,浏览器可以在应用程序需要时按需下载其他代码块。

您可以在此处了解有关 Webpack 代码拆分的更多信息。

在您的 Web 服务器上启用 Gzip

React 应用的 bundle JS 文件通常很大,所以为了让网页加载更快,我们可以在 Web 服务器(Apache、Nginx 等)上启用 Gzip

现代浏览器都支持并自动协商 HTTP 请求的 Gzip 压缩。 启用 Gzip 压缩可以将传输响应的大小减少多达 90%,这可以显着减少下载资源的时间,减少客户端的数据使用量,并缩短首次呈现页面的时间。

查看有关如何启用压缩的 Web 服务器文档:

  • 阿帕奇:使用 mod_deflate
  • Nginx:使用 ngx_http_gzip_module

使用 Eslint-plugin-react

您应该将 ESLint 用于几乎所有 JavaScript 项目。 React 也不例外。

使用eslint-plugin-react ,您将强迫自己适应 React 编程中的许多规则,从长远来看,这些规则可以使您的代码受益,并避免由于编写不佳的代码而发生的许多常见问题和问题。

让您的 React Web 应用程序再次快速运行

要充分利用 React,您需要利用它的工具和技术。 React Web 应用程序的性能在于其组件的简单性。 压倒性的渲染差异算法会使您的应用程序以令人沮丧的方式表现不佳。

在优化应用之前,您需要了解 React 组件的工作原理以及它们在浏览器中的呈现方式。 React 生命周期方法为您提供了防止组件不必要地重新渲染的方法。 消除这些瓶颈,您将获得用户应得的应用程序性能。

尽管有更多优化 React Web 应用程序的方法,但微调组件以仅在需要时更新会产生最佳的性能改进。

你如何衡量和优化你的 React Web 应用程序性能? 在下面的评论中分享它。

相关: Stale-while-revalidate Data Fetching with React Hooks: A Guide