作者都是各自领域经过审查的专家,并撰写他们有经验的主题. 我们所有的内容都经过同行评审,并由同一领域的Toptal专家验证.
威廉·王的头像

威廉王

拥有从自由职业者到CTO的经验, William是一名全栈开发人员,拥有LAMP堆栈和反应的背景.js.

专业知识

工作经验

16

分享

自推出以来,反应已经改变了前端开发人员构建web应用程序的方式. 使用虚拟DOM, 反应使UI更新尽可能高效, 让你的web应用更灵活. 但是,为什么中等规模的反应 web应用仍然表现不佳呢?

线索就在于你是如何使用反应的.

一个现代的前端库 反应 不会神奇地让你的应用更快. 它要求开发人员了解反应是如何工作的,以及组件在组件生命周期的各个阶段是如何生存的.

与反应, 通过测量和优化组件呈现的方式和时间,您可以获得它所提供的许多性能改进, 该框架提供了促进反应性能优化所需的工具.

通过优化组件的render-diff过程来加速反应应用程序.

在本教程中,我们将专注于优化反应性能. 您将学习如何测量反应组件的性能并对其进行优化,以构建性能更高的反应 web应用程序. 您还将学习一些JavaScript最佳实践如何帮助您的反应 web应用程序提供更流畅的用户体验.

反应是如何工作的?

在我们深入了解反应优化技术之前, 我们需要更好地理解反应是如何工作的.

的核心 反应的发展,您拥有简单而明显的JSX语法,以及反应的构建和比较能力 虚拟延迟性肌肉酸痛. 自发布以来,反应已经影响了许多其他前端库. 类库,如Vue.js也依赖于虚拟dom的概念.

下面是反应的工作原理:

每个反应应用程序都从一个根组件开始, 它是由树形结构中的许多成分组成的. 反应中的组件是基于接收到的数据(道具和状态)呈现UI的“函数”.

我们可以把它表示为 F.

UI = F(data)

用户与UI交互并导致数据更改. 交互是否涉及点击按钮, 点击图像, 拖拽列表项, 调用api的AJAX请求, 等.,所有这些相互作用只会改变数据. 它们永远不会直接改变UI.

在这里, 数据是定义web应用程序状态的一切, 而不仅仅是存储在数据库中的数据. 甚至前端状态(例如.g.(当前选择了哪个选项卡或当前是否选中了某个复选框)都是该数据的一部分.

只要数据有变化, 反应使用组件函数来重新呈现UI, 但实际上:

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

反应通过对两个版本的虚拟DOM应用比较算法来计算当前UI和新UI之间的差异.

变化= Diff(UI1, UI2)

然后反应继续只将UI更改应用于浏览器上的实际UI.

当与组件关联的数据发生变化时, 反应决定是否需要实际的DOM更新. 这使得反应可以避免在浏览器中进行可能代价高昂的DOM操作, 例如创建DOM节点和访问不必要的现有节点.

在任何反应应用程序中,组件的重复区分和渲染可能是反应性能问题的主要来源之一. 构建一个无法有效协调差分算法的反应应用程序, 让整个应用重复渲染可能会导致令人沮丧的缓慢体验.

从哪里开始优化?

但我们优化的到底是什么?

你看,在最初的渲染过程中,反应构建了一个这样的DOM树:

反应组件的虚拟DOM

给定部分数据的变化, 我们希望反应做的是只重新渲染那些直接受到更改影响的组件(并且可能跳过其他组件的diff过程):

反应渲染最优数量的组件

然而,反应最终做的是:

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

在上面的图片中, 所有的黄色节点都被渲染和渲染, 导致浪费时间/计算资源. 这是我们将主要把我们的优化工作. 将每个组件配置为仅在必要时渲染差异将允许我们回收这些浪费的CPU周期.

反应库的开发人员考虑到了这一点,并为我们提供了一个钩子:一个让我们告诉反应何时可以跳过渲染组件的函数.

测量第一

正如罗布·派克(Rob Pike)所言,这是他的作品之一 编程规则:

测量. 在你测量完之前不要调整速度, 即便如此,也不要这样做,除非代码的一部分压倒了其他部分.

不要开始优化你觉得可能会减慢应用速度的代码. 相反,让反应性能测量工具来指导您.

反应有一个 强大的工具 就为了这个. 使用 react-addons-perf 库,你可以得到你的应用程序的整体性能的概述.

用法很简单:

从react-addons-perf中导入性能
性能.开始();
//使用应用程序
性能.停止();
性能.printWasted ();

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

渲染时浪费时间的组件表

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

让基准管理更进一步

如果你是视觉型的人,那么 react-perf-tool 这正是你需要的吗.

react-perf-tool 是基于 react-addons-perf 图书馆. 它给了你一个更直观的方式 调试性能 你的反应应用程序. 它使用底层库来获取测量值,然后将其可视化为图形.

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

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

反应应该更新组件吗?

默认情况下, 反应将运行, 呈现虚拟DOM, 并比较树中每个组件的道具或状态变化的差异. 但这显然是不合理的.

随着应用的发展, 试图在每次操作时重新渲染和比较整个虚拟DOM最终会减慢速度.

反应为开发人员提供了一种简单的方法来指示组件是否需要重新呈现. 这就是 should组件Update 方法开始发挥作用.

函数should组件Update(nextProps, nextState) {
    返回true;
}

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

这为您提供了一种简单的方法来控制render-diff过程. 当你需要阻止一个组件被重新渲染时,只需返回即可 从函数中. 函数内部, 你可以比较当前和下一组道具和状态来决定是否需要重新渲染:

函数should组件Update(nextProps, nextState) {
    返回nextProps.id != =这.道具.id;
}

使用反应.Pure组件

为了简化和自动化这个优化技术, 反应提供了所谓的“纯”组件. A 反应.Pure组件 就像一个 反应.组件 它实现了 should组件Update () 函数用浅道具和状态比较.

A 反应.Pure组件 或多或少相当于这个:

类My组件扩展反应.组件{
    should组件Update(nextProps, nextState) {
        返回shallowCompare(这.道具,nextProps) && shallowCompare(这.状态,nextState);
    }
    …
}

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

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

使数据不可变

如果你可以用一个 反应.Pure组件 但仍然有一种有效的方式来告诉任何复杂的道具或状态何时自动改变? 这就是不可变数据结构简化工作的地方.

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

你可以用 Object.分配 or _.扩展 (从强调.js或Lodash):

const newValue2 =对象.分配({},oldValue);
const newValue2 = _.扩展({},oldValue);

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

var map1 =不可变.Map({a:1, b:2, c:3});
Var map2 = map1.集(" b ", 2);
断言(map1.= (map2) === true);
Var map3 = map1.组(b, 50);
断言(map1.= (map3) === 假);

在这里, 不可变的.Map 是图书馆提供的吗 不可变的.js.

每次用它的方法更新地图时 ,只有集操作改变了基础值,才会返回一个新的映射. 否则,将返回相同的映射.

您可以了解有关使用不可变数据结构的更多信息 在这里.

更多反应性能优化技术

使用生产版本

在开发反应应用程序时,你会看到非常有用的警告和错误消息. 这使得在开发过程中识别bug和问题成为一件乐事. 但这是以性能为代价的.

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

如果您使用 create-react-app,然后你就可以跑了 NPM运行构建 在没有这些额外代码的情况下生成生产构建. 如果您直接使用Webpack,则可以运行 webpack - p (这相当于 Webpack——优化——最小化——定义进程.env.NODE_ENV = "生产”.

早期绑定函数

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

//在每次渲染()时创建一个新的' handleUpload '函数

// ...内联箭头函数也是如此
 这.handleUpload(files)} />

这将导致 呈现() 函数在每次渲染上创建一个新函数. 做同样事情的更好的方法是:

类应用程序扩展反应.组件{
    构造函数(道具){
        超级(道具);
        这.handleUpload =这个.handleUpload.bind ();
    }
    呈现(){
        …
        
        …
    }
}

使用多个块文件

对于单页反应 web应用, 我们经常将所有前端JavaScript代码打包到一个小文件中. 这适用于小型到中等规模的web应用程序. 但随着应用程序的发展, 将这个捆绑的JavaScript文件交付给浏览器本身可能会成为一个耗时的过程.

如果你正在使用Webpack来构建反应应用, 你可以利用它的代码分割功能,将你构建的应用代码分成多个“块”,并根据需要将它们交付给浏览器.

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

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

这种分裂在两个方面有帮助. 它帮助浏览器缓存那些不太频繁变化的资源. 它还将帮助浏览器利用并行下载的优势,以潜在地减少加载时间.

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

您可以了解有关Webpack代码拆分的更多信息 在这里.

在您的Web服务器上启用Gzip

反应应用的bundle JS文件通常非常大, 所以要使网页加载更快, 我们可以在web服务器(Apache)上启用Gzip, Nginx, 等.)

现代浏览器都支持并自动协商HTTP请求的Gzip压缩. 启用Gzip压缩可以将传输的响应大小减少高达90%, 哪一种方法可以显著减少下载资源的时间, 减少客户端的数据使用量, 并改善第一次渲染页面的时间.

查看web服务器的文档,了解如何启用压缩:

使用Eslint-plugin-react

你应该在几乎所有JavaScript项目中使用ESLint. 反应也不例外.

eslint-plugin-react, 你将强迫自己适应反应编程中的许多规则,这些规则可以使你的代码从长期运行中受益,并避免由于代码编写不当而出现的许多常见问题和问题.

让你的反应 Web应用再次快速

为了充分利用反应,你需要利用它的工具和技术. 反应 web应用的性能在于其组件的简单性. 过度使用渲染差异算法会让你的应用表现糟糕,令人沮丧.

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

尽管有更多的优化方法, 当涉及到反应性能改进时, 微调组件,使其仅在需要时更新,可获得最佳结果.

请在下面的评论中分享你对反应性能最佳实践的看法.

了解基本知识

  • 你应该首先优化哪个反应组件?

    您应该总是从度量代码的性能开始. 有时候看似“简单”的代码可能会导致你的应用变慢. 在深入优化之前,用它所提供的工具衡量你的反应代码的性能.

  • 我如何使用反应.Pure组件?

    不同的反应.组件,一个反应.Pure组件只会在组件的状态或道具发生变化时才会重新渲染. 它执行浅层比较来检测更改. 您可以利用这种状态下的不可变数据结构来利用这种行为,当引用的内容发生变化时,这种状态就会发生变化.

就这一主题咨询作者或专家.
预约电话
威廉·王的头像
威廉王

位于 昆明,云南,中国

成员自 2016年12月12日

作者简介

拥有从自由职业者到CTO的经验, William是一名全栈开发人员,拥有LAMP堆栈和反应的背景.js.

Toptal作者都是各自领域经过审查的专家,并撰写他们有经验的主题. 我们所有的内容都经过同行评审,并由同一领域的Toptal专家验证.

专业知识

工作经验

16

世界级的文章,每周发一次.

订阅意味着同意我们的 隐私政策

世界级的文章,每周发一次.

订阅意味着同意我们的 隐私政策

Toptal开发者

加入总冠军® 社区.