本文概述
自从引入以来, React改变了前端开发人员构建Web应用程序的方式。借助虚拟DOM, React使UI更新变得前所未有的高效, 从而使你的Web应用程序更加灵活。但是, 为什么中等大小的React Web应用程序仍然表现不佳?
好吧, 线索在于你如何使用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中的组件是”功能”, 它们根据接收到的数据(属性和状态)呈现UI。
我们可以将其表示为F。
UI = F(data)
用户与UI交互并导致数据更改。交互是否涉及单击按钮, 点击图像, 在列表项周围拖动, AJAX请求调用API等, 所有这些交互都只会更改数据。它们永远不会导致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应用程序, 导致整个应用程序被重复渲染会导致令人沮丧的缓慢体验。
从哪里开始优化?
但是, 我们最优化的是什么呢?
你会看到, 在最初的渲染过程中, React会构建如下的DOM树:
给定一部分数据更改, 我们希望React要做的是仅重新呈现受更改直接影响的组件(并可能跳过其余组件的diff处理):
但是, React最终要做的是:
在上图中, 所有黄色节点都被渲染和扩散, 从而浪费了时间/计算资源。这是我们主要进行优化工作的地方。将每个组件配置为仅在必要时仅进行render-diff, 这将使我们能够回收这些浪费的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, 并比较树中每个组件的差异, 以了解其属性或状态的任何变化。但这显然是不合理的。
随着你的应用程序的增长, 在每次操作时尝试重新渲染和比较整个虚拟DOM最终都会减慢速度。
React为开发人员提供了一种简单的方法来指示组件是否需要重新渲染。这是shouldComponentUpdate方法起作用的地方。
function shouldComponentUpdate(nextProps, nextState) {
return true;
}
当此函数对任何组件返回true时, 将允许触发render-diff过程。
这为你提供了控制渲染差异过程的简单方法。每当你需要阻止组件完全重新渲染时, 只需从函数中返回false即可。在函数内部, 你可以比较当前和下一组道具和状态, 以确定是否需要重新渲染:
function shouldComponentUpdate(nextProps, nextState) {
return nextProps.id !== this.props.id;
}
使用React.PureComponent
为了简化和自动化这种优化技术, React提供了所谓的”纯”组件。 React.PureComponent就像React.Component一样, 它通过浅层支持和状态比较实现了shouldComponentUpdate()函数。
React.PureComponent或多或少与此等效:
class MyComponent extends React.Component {
shouldComponentUpdate(nextProps, nextState) {
return shallowCompare(this.props, nextProps) && shallowCompare(this.state, nextState);
}
…
}
由于它仅执行浅表比较, 因此仅在以下情况下才可能有用:
- 你的道具或状态包含原始数据。
- 你的道具和状态具有复杂的数据, 但是你知道何时调用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.js由Immutable.js库提供。
每次使用其方法集更新地图时, 仅当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’”。
尽早绑定功能
在render函数中, 经常看到绑定到组件上下文的函数。当我们使用这些函数来处理子组件的事件时, 通常就是这种情况。
// 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应用程序的捆绑JS文件通常很大, 因此为了使网页加载更快, 我们可以在Web服务器(Apache, Nginx等)上启用Gzip
现代浏览器均支持HTTP请求并自动协商Gzip压缩。启用Gzip压缩可以将传输的响应的大小最多减少90%, 这可以大大减少下载资源的时间, 减少客户端的数据使用量, 并缩短首次呈现页面的时间。
有关如何启用压缩的信息, 请查阅Web服务器的文档:
- Apache:使用mod_deflate
- Nginx:使用ngx_http_gzip_module
使用Eslint-plugin-react
你应该对几乎所有JavaScript项目都使用ESLint。 React没什么不同。
使用eslint-plugin-react, 你将迫使自己适应React编程中的许多规则, 这些规则从长远来看可以使你的代码受益, 并避免由于编写不良的代码而导致的许多常见问题和问题。
使你的React Web Apps再次快速
要充分利用React, 你需要利用其工具和技术。 React Web应用程序的性能在于其组件的简单性。压倒渲染差异算法会使你的应用以令人沮丧的方式表现不佳。
在优化应用程序之前, 你需要了解React组件如何工作以及如何在浏览器中呈现它们。 React生命周期方法为你提供了防止不必要地重新渲染组件的方法。消除这些瓶颈, 你将获得用户应得的应用程序性能。
尽管还有更多优化React Web应用程序的方法, 但微调组件以仅在需要时进行更新才能带来最佳的性能改进。
你如何衡量和优化React Web应用程序性能?在下面的评论中分享。
相关:使用React Hooks重新验证时失效的数据:指南