十、提升你的应用的性能

web 应用程序的有效性能对于提供良好的用户体验和改进转换至关重要。React 库实现了不同的技术来快速渲染我们的组件,并尽可能少地接触文档对象模型DOM)。对 DOM 应用更改通常代价高昂,因此最小化操作数量至关重要。

但是,在某些特定的场景中,React 无法优化流程,开发人员需要实施特定的解决方案以使应用程序顺利运行。

在本章中,我们将介绍 React 的基本概念,并学习如何使用一些 api 来帮助库找到更新 DOM 的最佳路径,而不会降低用户体验。我们还将看到一些常见的错误,这些错误可能会损害我们的应用程序并使它们变得更慢。

我们应该避免为了 it 而优化我们的组件,并且只有在需要时才应用我们将在以下部分中看到的技术,这一点很重要。

在本章中,我们将介绍以下主题:

  • 如何协调工作以及我们如何帮助您更好地使用密钥
  • 常见优化技术和常见性能相关错误
  • 使用不可变数据意味着什么以及如何使用
  • 使我们的应用程序运行更快的有用工具和库

技术要求

要完成本章,您需要以下内容:

  • Node.js 12+
  • Visual Studio 代码

您可以在本书的 GitHub 存储库中找到本章的代码 https://github.com/PacktPublishing/React-17-Design-Patterns-and-Best-Practices-Third-Edition/tree/main/Chapter10

调解

大多数情况下,默认情况下 React 足够快,您不需要再做任何事情来提高应用程序的性能。React 使用不同的技术优化屏幕上组件的渲染。

当 React 必须显示一个组件时,它递归调用它的render方法及其子级的render方法。组件的render方法返回 React 元素树,React 使用这些元素来决定更新 UI 必须执行哪些 DOM 操作。

每当组件状态发生变化时,React 会再次调用节点上的render方法,并将结果与之前的 React 元素树进行比较。该库足够智能,可以计算出在屏幕上应用预期更改所需的最小操作集。这一过程称为和解,由 React 透明管理。多亏了这一点,我们可以轻松地描述我们的组件如何以声明的方式查看给定的时间点,并让库完成其余的工作。

React 尝试在 DOM 上应用尽可能少的操作,因为接触 DOM 是一项昂贵的操作。

但是,比较两个元素树也不是免费的,React 做出两个假设以降低其复杂性:

  • 如果两个元素具有不同的类型,则它们呈现不同的树。
  • 开发人员可以使用键在不同的渲染调用中将子级标记为稳定的。

从开发人员的角度来看,第二点很有趣,因为它为我们提供了一个工具,帮助我们更快地反应和渲染视图。

默认情况下,当返回到 DOM 节点的子节点时,React 会同时迭代两个子节点列表,并且每当出现差异时,就会创建一个变异。

让我们看一些例子。在子元素末尾添加元素时,在以下两棵树之间进行转换将非常有效:

<ul>
 <li>Carlos</li>
  <li>Javier</li>
</ul>

<ul>
 <li>Carlos</li>
 <li>Javier</li>
 <li>Emmanuel</li>
</ul>

两个<li>Carlos</li>树通过 React 匹配两个<li>Javier</li>树,然后插入<li>Emmanuel</li>树。

如果简单地实现,在开始时插入元素会产生较差的性能。如果我们看一下这个示例,在这两棵树之间转换时,它的效果非常差:

<ul>
 <li>Carlos</li>
  <li>Javier</li>
</ul>

<ul>
  <li>Emmanuel</li>
 <li>Carlos</li>
 <li>Javier</li>
</ul>

每个孩子都会因 React 而发生变异,而不是意识到它可以保持子树线<li>Carlos</li><li>Javier</li>完好无损。这可能是个问题。当然,这个问题是可以解决的,解决方法是 React 支持的key属性。接下来我们来看看。

钥匙

子级拥有键,React 使用这些键在后续树和原始树之间匹配子级。通过在前面的示例中添加一个键,可以提高树转换的效率:

<ul>
 <li key="2018">Carlos</li>
  <li key="2019">Javier</li>
</ul>

<ul>
  <li key="2017">Emmanuel</li>
 <li key="2018">Carlos</li>
 <li key="2019">Javier</li>
</ul>

React 现在知道2017键是新的,20182019键刚刚移动。

找到钥匙并不难。您将要显示的元素可能已经具有唯一的 ID。因此,密钥只能来自您的数据:

<li key={element.id}>{element.title}</li>

您可以将新 ID 添加到模型中,也可以通过内容的某些部分生成密钥。钥匙只能在兄弟姐妹中是唯一的;它不必是全球唯一的。数组中的项索引可以作为键传递,但现在认为这是一种不好的做法。但是,如果这些项目从未被记录下来,这可能会很好地工作。重新订购将严重影响性能。

如果您使用map函数呈现多个项目,并且未指定 key 属性,则会收到以下消息:警告:数组或迭代器中的每个子级都应具有唯一的“key”属性。

让我们在下一节学习一些优化技术。

优化技术

需要注意的是,在本书的所有示例中,我们使用的应用程序要么是使用create-react-app创建的,要么是从头创建的,但始终使用 React 的开发版本。

使用 React 的开发版本对于编码和调试非常有用,因为它为您提供了修复各种问题所需的所有信息。然而,所有的检查和警告都会带来成本,这是我们希望在生产中避免的。

因此,我们应该对应用程序进行的第一个优化是构建 bundle,将NODE_ENV环境变量设置为production。使用webpack非常简单,只需按以下方式使用DefinePlugin

new webpack.DefinePlugin({ 
  'process.env': { 
    NODE_ENV: JSON.stringify('production')
  }
})

为了获得最佳性能,我们不仅希望创建激活生产标志的捆绑包,还希望拆分我们的捆绑包,一个用于我们的应用程序,另一个用于node_modules

为此,您需要在webpack中使用新的优化节点:

optimization: {
  splitChunks: {
    cacheGroups: {
      default: false,
      commons: {
        test: /node_modules/,
        name: 'vendor',
        chunks: 'all'
      }
    }
  }
}

由于 webpack 4 有两种模式,开发生产,默认情况下,生产模式是启用的,这意味着当您使用生产模式编译包时,代码将被缩小和压缩;可以使用以下代码块指定它:

{
  mode: process.env.NODE_ENV === 'production' ? 'production' : 
    'development',
}

您的webpack.config.ts文件应如下所示:

module.exports = {
  entry: './index.ts',
  optimization: {
    splitChunks: {
      cacheGroups: {
        default: false,
        commons: {
          test: /node_modules/,
          name: 'vendor',
          chunks: 'all'
        }
      }
    }
  },
  plugins: [
    new webpack.DefinePlugin({
      'process.env': {
        NODE_ENV: JSON.stringify('production')
      }
    })
  ],
  mode: process.env.NODE_ENV === 'production' ? 'production' : 
    'development'
}

通过此 webpack 配置,我们将获得非常优化的捆绑包,一个用于我们的供应商,另一个用于实际应用程序。

工具和库

在下一节中,我们将介绍一些技术、工具和库,这些技术、工具和库可以应用于我们的代码库,以监控和提高性能。

不变性

新的 React 钩子,比如React.memo,使用了一种针对道具的浅层比较方法,这意味着如果我们将一个对象作为道具传递,并且我们改变了它的一个值,我们就不会得到预期的行为。

事实上,肤浅的比较无法发现属性上的突变,组件永远不会重新渲染,除非对象本身发生更改。解决这个问题的一种方法是使用不可变数据,即一旦创建就无法变异的数据。

例如,我们可以在以下模式下设置状态:

const [state, setState] = useState({})

const obj = state.obj

obj.foo = 'bar'

setState({ obj })

即使更改了对象的foo属性的值,对该对象的引用仍然是相同的,并且浅层比较无法识别它。

我们可以做的是在每次修改对象时创建一个新实例,如下所示:

const obj = Object.assign({}, state.obj, { foo: 'bar' })

setState({ obj })

在这种情况下,我们得到一个新对象,其foo属性设置为bar,通过浅层比较可以找到差异。对于 ES6 和 Babel,还有另一种方法可以更优雅地表达相同的概念,那就是使用对象扩展操作符:

const obj = { 
  ...state.obj, 
  foo: 'bar' 
}

setState({ obj })

此结构比前一个结构更简洁,并且产生相同的结果,但在编写时,需要传输代码才能在浏览器中执行。

React 提供了一些不变性帮助程序,使使用不变性对象变得容易,还有一个名为immutable.js的流行库,它具有更强大的功能,但需要您学习新的 API。

巴别塔插件

还有几个有趣的Babel插件,我们可以安装并使用它们来提高 React 应用程序的性能。它们使应用程序更快,在构建时优化了部分代码。

第一个是 React constant elements transformer,它查找所有不随道具变化的静态元素,并从render(或功能组件)中提取它们,以避免不必要地调用_jsx

使用 Babel 插件非常简单。我们先用npm安装:

npm install --save-dev @babel/plugin-transform-react-constant-elements

您需要创建.babelrc文件并添加一个plugins键,其中包含一个数组,该数组的值为我们要激活的插件列表的值:

{ 
  "plugins": ["@babel/plugin-transform-react-constant-elements"] 
}

我们可以选择用来提高性能的第二个 Babel 插件是 React-inline-elements 转换,它将所有 JSX 声明(或_jsx调用)替换为更优化的版本,以加快执行速度。

使用以下命令安装插件:

npm install --save-dev @babel/plugin-transform-react-inline-elements

接下来,您可以轻松地将插件添加到.babelrc文件中的插件数组中,如下所示:

{
  "plugins": ["@babel/plugin-transform-react-inline-elements"] 
}

这两个插件都应该只在生产环境中使用,因为它们使开发模式下的调试更加困难。到目前为止,我们已经学习了很多优化技术,以及如何使用 webpack 配置一些插件。

总结

我们的性能之旅已经结束,现在我们可以优化我们的应用程序,为用户提供更好的用户体验。

在本章中,我们了解了协调算法的工作原理,以及 React 如何始终尝试采用最短路径将更改应用于 DOM。我们还可以通过使用这些键来帮助库优化其工作。一旦您发现了瓶颈,就可以应用我们在本章中看到的其中一种技术来解决问题。

我们已经了解了如何以适当的方式重构和设计组件的结构,从而提高性能。我们的目标是让小组件以最好的方式完成一件事。在本章的最后,我们讨论了不变性,我们已经了解了为什么不改变数据以使React.memoshallowCompare发挥作用是很重要的。最后,我们运行了不同的工具和库,它们可以使您的应用程序更快。

在下一章中,我们将介绍如何使用 Jest、React 测试库和 React 开发工具进行测试和调试。