使 React 具有反应性:追求高性能、易于维护的 React 应用程序 | Mendix

跳到主要内容

让 React 更具响应性:追求高性能、易于维护的 React 应用

应用程序开发博客背景

2 年 3 月 2016 日编辑:Mobservable 已更名为 MobX

如何构建超快的 React 应用?最近,我们开始在一个大型项目中使用 React,得益于其构建组件的结构化方式和快速虚拟 DOM,它为我们提供了巨大的帮助,从而节省了大量的 UI 更新。这个项目的美妙之处在于它面临着一些不错的挑战;它需要在浏览器中绘制数千个对象,而这些对象彼此高度耦合。一个对象的值可能会被任意数量的其他对象使用,因此微小的更改可能需要更新 UI 中许多不相关的部分。这些值可能会通过用户的拖放操作进行更新,因此为了保持 UI 的响应性,所有更新和重新绘制都必须在 40 毫秒内完成。尽管普通的 React 速度很快,但我们很快就发现仅靠 React 无法完成这项工作。

因此,我们开始寻找一种解决方案,既能提供所需的性能,又能让我们的代码库根据 React 原则进行维护。简而言之,我们想要一个 优雅 解决方案。因此,我们尝试利用函数式响应式编程领域中的一个概念,即 可观察的。可观察对象的卖点是所有计算都会自动检测它们使用的其他可观察对象。当其中一个可观察对象将来发生变化时,计算将自动重新评估。 可观察的 是其他 UI 框架(如 Ember 和 Knockout)中使用的概念。我们发现,如果我们所有的模型对象都变成 可观察的 我们所有的 React 组件都变成了 观察员 模型,我们不需要再施加任何魔法来确保 UI 的相关部分(且只有相关部分)得到更新。继续阅读,了解所有精彩之处。结尾处有偶数!

让我们从一个简单的例子开始,让这个例子不那么理论化(或者说不那么繁琐,如果你愿意的话)。想象一个代表一家小商店的 React 应用。里面有一些文章,还有一个购物车,你可以把其中一些文章放进去。就像这样:

应用示例截图

噗, 那里 它存在于现实生活中。

数据模型

首先,让我们定义数据模型。有带有名称和价格的商品,还有购物车,总成本基于其条目的总和。每个条目都引用一篇文章,存储金额并具有派生价格。我们的数据模型中的关系如下所示。空心项目符号表示派生数据,如果其他数据发生变化,则应更新这些数据,其在 UI 中的表示也应更新。因此,即使在这个简单的模型中,也有大量数据在流动,当内容发生变化时,需要进行大量 UI 更新。

数据模型图表

让我们编制一个需求清单:

  • 如果某件商品的价格发生变化,所有相关的购物车条目都应重新评估其价格。
  • .. 购物车的总成本也应如此。
  • 如果购物车中的商品数量发生变化,则总费用应更新。
  • 如果文章被重命名,那么其视图也应该更新
  • 如果文章被重命名,则相关购物车条目的视图应该更新
  • 如果有新商品添加到购物车...
  • 等等等等

现在,我们的 UI 问题要点可能已经很清楚了。作为一名程序员,您不想编写样板代码来处理所有可能的更新,但如果您的应用程序在每次数据更改时总是重新呈现,您的用户可能不得不等待很长时间。

因此,让我们一劳永逸地解决这个问题并写下我们的数据模型:

好吧,这不太难,对吧?上面的构造函数严重依赖于 手机 库,它提供了可观察概念的独立实现(它应该可以像 React 一样轻松地与其他基于 JavaScript 的库结合)。 props 函数根据提供的键和值的类型在目标对象上创建新的可观察属性。由于所有属性都具有可观察性,因此上述函数会在其某些依赖项发生更改时自动(且仅)更新。这立即满足了我们的一些要求,例如 total 当添加新条目、商品价格发生变化等时,购物车会自动更新。

用户界面

眼见为实,让我们围绕这个模型构建一个用户界面。我们创建一些 React 组件来呈现我们的初始数据。以下 JSX 代码片段显示购物车上的视图,呈现购物车中的所有条目并显示购物车的总价。您可以想象,应用程序中的其他组件(如文章上的视图)非常相似。

很简单,对吧?CartView 组件接收购物车,使用 CartEntryView 呈现其总数和单个项目,然后打印相关文章的名称和所需文章的数量。根据 React 最佳实践,每个列出的项目都应该具有唯一性,因此我们为每个条目分配一个任意但不可变的 ID。删除按钮将此数量减一,如果减至零,则整个条目将从购物车中删除。请注意, removeArticle 函数中我们是否指出了应该更新 UI。

下一个重要步骤是强制这些组件与数据模型保持同步,例如当条目被删除时。正如您可以从渲染代码中轻松确定的那样,存在许多可能的数据转换;文章数量可以更改,购物车总成本可以更改,文章名称可以更改,甚至条目和文章​​之间的引用也可以更改。我们将如何监听所有这些变化?

嗯,这很简单。只需使用 mobxReact.observer 来自 mobx-react 包装到每个组件,这足以满足我们所有其他要求:

等等,什么,就这些?是的,只需查看演示和上述内容的源代码即可 JS小提琴。那么这里发生了什么? observer 函数为我们做了两件事。首先,它将组件的渲染函数变成了可观察函数。其次,组件本身被注册为该函数的观察者,这样每次渲染变得陈旧时,都会强制重新渲染。因此,此函数(如果使用 ES6,则为装饰器)确保每当可观察数据发生变化时,只更新 UI 的相关部分。只需在示例应用中试用一下,同时留意日志面板,看看 UI 是如何根据您的实际操作和实际数据进行更新的:

  • 尝试重命名购物车中没有的商品
  • 将商品添加到购物车,然后重命名
  • 将商品添加到购物车并更新其价格
  • 将其从购物车中移除,再次更新其价格
  • ...等等。您会注意到,每次执行操作时,最少量的组件都会被重新渲染。

最后,由于每个组件都跟踪自己的依赖项,因此通常不需要显式地重新渲染组件的子项。例如,如果重新渲染了购物车的总计,则无需重新渲染条目。React 自己的 PureRenderMixin 确保这样的事情不会发生。

数字

那么我们取得了什么成果呢?为了进行比较, 开始 您可以找到完全相同的应用程序,但没有可观察对象和简单的“重新渲染所有内容”方法。只有几篇文章时,您不会注意到任何差异,但一旦文章数量增加,性能差异就会变得非常明显。

 

创建和呈现文章图表更新文章图表

无论有没有可观察对象,创建大量数据和组件的行为都非常相似。但是一旦数据发生变化,可观察对象就开始真正发挥作用了。更新 10 个项目中的 10,000 篇文章的速度大约快了 2.5 倍!250 秒缩短到 10 毫秒。这就是滞后体验和非滞后体验之间的区别。这种差异从何而来?让我们首先看一下在没有可观察对象的“更新 10000 篇文章列表中的 XNUMX 篇文章”场景中运行更新后的 React 渲染报告:

日志截图

如您所见,所有两万个 ArticleViews 和 CartEntryViews 都进行了重新渲染。但是,根据 React 的数据,在总共 2,145 毫秒的渲染时间中,有 2,433 毫秒被浪费了。浪费的意思是:执行渲染函数所花费的时间实际上并没有导致真实 DOM 的更新。这强烈表明,如果组件很多,那么天真地重新渲染所有内容会浪费大量的 CPU 时间。为了进行比较,以下是使用可观察对象时相同场景的报告:

日志截图

这是一个很大的区别!重新渲染组件的数量不是 20,006 个,而是 31 个。更重要的是,没有报告浪费!这意味着每个重新渲染的组件实际上都会改变 DOM 中的某些内容。这正是我们想要通过使用可观察对象来实现的!

从报告中可以清楚地看出,剩余渲染时间的大部分(总共 243 毫秒中的 267 毫秒)都花在渲染 CartView 上,而 CartView 的重新渲染只是为了刷新购物车的总成本。但是重新渲染 CartView 也意味着重新访问所有一万个条目,以查看 CartEntryViews 的任何参数是否发生了变化。因此,只需将 CartView 的总数放在其自己的组件 CartTotalView 中,只要总成本发生变化,就可以跳过整个 CartView 的渲染。这将我们的渲染时间进一步缩短到大约 60 毫秒(参见上图中的“优化”系列)。这比我们原始 React 应用程序中的相同更新快大约 40 倍!

结语

通过使用可观察对象,我们构建的应用程序比单纯重新渲染所有组件的应用程序快一个数量级。而且,同样重要的是(对于您作为程序员而言),我们这样做并没有损害代码的可维护性。只需查看上面链接的两个 JSFiddle 的源代码即可;这两个清单非常相似,并且都同样方便使用。

我们是否可以使用其他技术实现相同的效果?也许。例如,ImmutableJS 也通过仅更新接收更改数据的组件来使 React 渲染非常快。但是,您必须在数据模型上做出更大的让步。毕竟,在我看来,可变类最终比不可变类更方便使用。此外,不可变数据结构无法帮助您保持计算值的更新。因此,使用不可变数据,更改文章名称会非常快速地重新渲染 ArticleView,但仍然不会使引用同一篇文章的任何现有 CartEntryViews 无效。

另一种可用于优化 React 应用的技术是为数据中的每个可能变化创建事件,并在适当的时间和适当的组件中注册(取消)这些事件的侦听器。但这会导致大量样板代码,维护起来容易出错。此外,我想我太懒了,不想做这样的事情。

顺便说一句,我强烈建议使用控制器或动作调度程序作为更新模型数据的抽象,以保持项目中关注点的清晰分离。

总而言之,在大型项目中,将 React 与 Observables 结合起来效果非常好,有时我看到数据变化正确地更新了 UI,尽管我当时甚至还没有想到,而且没有遇到任何性能问题。所以对我来说,我会把弄清楚何时以及如何尽快更新 UI 的艰苦工作留给 React 和 Observables,并专注于编码的有趣部分 :)。

在以下网站讨论此帖子 黑客新闻.

相关资源

选择你的语言