优化React前端性能的五个技巧
在现代 Web 应用开发中,性能是获得更好的用户体验的关键。使用 React 能够产生直观的用户界面,为众多应用提供更强大的用户体验,而且无需花费很多的精力来显式地优化性能。但是,开发人员在构建大型应用时,往往受困于性能问题,我们可以采用各种方法来进一步优化 React 应用的性能。
在本文中,我将讨论提升 React 前端性能的五个技巧。
React 使用了虚拟 DOM(VDOM)的概念,在这种概念中,像 ReactDOM 这样的库会在内存中维护用户界面的“虚拟”表述,并使其与“实际”的 DOM 保持同步。这个过程叫做协调(reconciliation)。
即便只更新已修改的 DOM 节点,重新渲染也会需要时间。生命周期函数 shouldComponentUpdate() 会在重新渲染过程开始之前被调用,这通常都不是什么问题,但是,如果延迟很明显的话,我们可以通过覆盖 shouldComponentUpdate() 来加速这一过程。该函数的默认实现会返回 true,即让 React 来处理更新,如下所示:
shouldComponentUpdate(nextProps, nextState) {
return true;
}
如果你知道自己的组件在某些情况下不需要更新的话,那么可以从 shouldComponentUpdate() 中返回 false 来完全省略渲染过程。
在 React“纯”组件的帮助下,这种优化策略可以变得更加容易、更加自动化。PureComponent 使用浅层(shallow)属性和状态对比来实现其 shouldComponentUpdate()。
这意味着它会对原始数据类型的值和对象的引用进行对比。因此,在使用 React.PureComponent 时,我们需要满足如下两个先决条件:
组件的 state/props 应该是不可变对象,不应该有太多层级的嵌套对象,并且应该包含原始类型的数据。
所有的子组件应该是“纯”组件或函数式组件。
假设我们要思考一种有效的方式来自动探测复杂属性或状态变更,这里就是不可变数据结构的用武之地了。
使用不可变数据结构背后的理念很简单:与其直接对包含复杂数据的对象进行修改,不如创建一个包含数据变更的副本。因此,识别对象变化就可以像比较两个对象的引用一样容易。
为了自动检查复杂的状态变更,可以组合使用 React.PureComponent 和不可变数据结构。例如,借助像 Redux 这样的状态管理工具,如果应用中的状态是不可变的,我们可以将所有的状态对象保存到一个单独的存储中,这样就可以很容易地实现撤销和重做功能。
如果能够使用提供不可变数据结构的库会更好。在处理高度嵌套的对象时,更新不可变的对象可能是一项挑战。如果你遇到这种问题,可以考虑使用 Immer 或 immutability-helper。这些库能够让我们创建更加易读的代码,同时保留了不可变性的优势。
使用不可变数据结构会带来一些收益:
没有副作用(side effect)。
创建、测试和使用不可变的数据对象都很简单。
它们协助我们编写的代码能够快速检测状态变更,而不需要重复检查数据。
它们能够避免时序耦合(temporal coupling)。
当创建 React 应用时,你会看到很多有用的警告和错误信息。在开发软件的过程中,它们能够让发现缺陷和问题变得更加容易。但是,它们在性能方面是有代价的。
因此,如果你要做基准测试或者遇到性能问题,请使用最小化的生产化构建来测试你的 React 应用。终端用户不需要 React 在开发环境中运行的这些代码片段。这些不相关的代码都可以在生产环境中移除。
如果你使用 create-react-app 进行的项目初始化的话,那么可以使用 npm run build 来生成没有这些额外代码的生产化构建。如果你直接使用 Webpack 的话,那么可以运行 webpack -p(相当于 webpack — optimize-minimize — define process.env.NODE ENV=”’production’”)。
在你项目的 /build 子目录下,现在将会包含应用的生产化构建。请记住,这仅在生产化部署时才是必要的,在通常的开发中,依然可以使用 npm start。
在项目初期,你的 React 应用可能只有几个组件。但是,随着不断添加新的功能和依赖,应用也会随着时间的推移而不断增长。不知不觉中,你可能就会有一个非常庞大的生产化文件。
对于单页的 React 应用,我们通常会将所有的前端 JavaScript 代码打包到一个最小化的文件中。对于中小规模的应用来说,这是合理的。但是,当应用的规模扩大时,将这些打包的 JavaScript 代码发送到浏览器会耗费大量的时间,从而影响应用的性能。
如果你使用 Webpack 来构建 React 项目的话,那么可以使用它的代码拆分功能,也就是将要构建的应用代码分割成多个“块(chunk)”。例如,我们可以使用 CommonsChunkPlugin 将应用打包成两个独立的文件,也就是将供应商或第三方库的代码与我们的应用代码区分开,并按需传递给浏览器。这样的话,我们最后会得到名为 vendor.bundle.js 和 app.bundle.js 的两个文件。通过拆分文件,我们的浏览器能够减少缓存并且能够并行加载资源,以尽量减少加载时间的延迟。
按需拆分代码是 Webpack 的一个优秀特性。它可以将代码分割成较小的块,这些块可以在需要时加载。这实现了最小的初始下载量,减少了应用的加载时间。然后,浏览器可以根据应用的需要下载更多的代码块。
如果你正在展示一个包含大量数据的长列表,那么在给定的时间,你应该仅在可见的视口(viewport)内渲染一部分的数据。数据仅在视口中显示,当列表向下滚动时,更多的数据会被渲染出来。这种技术被称为“窗口化(windowing)”。该技术可以大大减少重新渲染组件所消耗的时间以及所生成的 DOM 节点的数量,因为每次它仅渲染很小的一部分数据行。
知名的窗口化库是 react-window 和 react-virtualized。它们提供了几个可重用的组件来展示数据表格、列表和表格化的数据。当然,如果你需要满足应用特定的需求,那么可以像 Twitter 那样不断地开发自己的窗口组件。
React Web 应用的性能是由其组件的简洁性决定的。因此,在解决性能问题之前,了解 React 组件的工作方式、渲染和 diffing 技术是至关重要的。在 React 生命周期方法的帮助下,我们可以避免不必要的组件渲染。如果消除了这些限制,应用能够像用户期望的那样流畅运行。所以,在增强 React 应用的性能时,你应该考虑所有的这些想法。
尽管还有其他各种方法来增强 React 应用的性能,但我希望这篇文章能帮助你获得优化 React 前端性能的最佳技巧。
Spotify移动工程平台迁移:将Android和iOS代码库迁移到Bazel