React-Redux 应用程序真的可以像 Backbone 一样扩展吗?即使重新选择。在移动

Posted

技术标签:

【中文标题】React-Redux 应用程序真的可以像 Backbone 一样扩展吗?即使重新选择。在移动【英文标题】:Can a React-Redux app really scale as well as, say Backbone? Even with reselect. On mobile 【发布时间】:2016-04-19 08:17:58 【问题描述】:

在 Redux 中,对 store 的每次更改都会在所有连接的组件上触发 notify。这让开发者的事情变得非常简单,但是如果你有一个包含 N 个连接组件的应用程序,而 N 非常大呢?

对商店的每次更改,即使与组件无关,仍然会在商店的reselected 路径上运行shouldComponentUpdate 和简单的=== 测试。这很快,对吧?当然,也许一次。但是N次,对于的变化?这种设计上的根本变化让我质疑 Redux 的真正可扩展性。

作为进一步的优化,可以使用_.debounce 批处理所有notify 调用。即便如此,对每个商店更改进行 N 个=== 测试处理其他逻辑,例如视图逻辑,似乎是达到目的的一种手段。

我正在开发一个拥有数百万用户的健康和健身社交移动网络混合应用程序,并且正在从 Backbone 过渡到 Redux。在这个应用程序中,用户会看到一个可滑动的界面,允许他们在不同的视图堆栈之间导航,类似于 Snapchat,除了每个堆栈都有无限的深度。在最流行的视图类型中,无限滚动条有效地处理提要项目的加载、渲染、附加和分离,例如帖子。对于参与度高的用户,滚动浏览成百上千条帖子,然后输入用户的提要,然后是另一个用户的提要等的情况并不少见。即使经过大量优化,连接组件的数量也会变得非常大。

另一方面,Backbone 的设计允许每个视图精确地聆听影响它的模型,将 N 减少到一个常数。

是我遗漏了什么,还是 Redux 对于大型应用程序存在根本缺陷?

【问题讨论】:

假设您connect 的组件数量远小于 N,这似乎很大程度上是一个 React 问题。如果shouldComponentUpdatefalse,组件的整个子树将不会重新渲染,这将有所帮助。听起来您仍将拥有一个非常大的组件树,因此您可能希望通过动态更改已安装的组件来做一些高级的事情。或许可以先花时间进行模拟,看看压力从哪里开始显现,并从那里测试策略。 对。您最关心的 N 是 top-level 树的数量。如果那些***树为 shouldComponentUpdate 返回 false,则不检查整个子树。 @acjay 在一个无限滚动器中(至少我的实现),不涉及 React,节点作为单独的容器进行管理。如果有一种同样高效的方式来管理无尽的滚动条,我会全神贯注,但在我的搜索中,我发现没有一个性能几乎与我的原始 JS 解决方案一样好。我们的非滚动组件(即<Platform> > <Stack> > <Page>)可以很好地管理它们的性能,并且是一个小得多的问题。我担心的性能损失是必须附加 100 多个无限滚动容器时(当用户滚动浏览提要时)。 在内部,如果帖子发生变化,React 不会重新渲染整个树。它将区分 DOM 并仅呈现它需要的内容。此外,您可以使用shouldComponentUpdate 来防止它出现在极端情况下。但是假设您已经在这部分代码中删除了 React,这会使您的问题变得不公平:这不是“Redux 是否可以很好地扩展”,而是“如果我以非设计方式使用 React 和 Redux 来创建一个很多额外的连接,它会很好地扩展吗?此外,无限滚动的一个常见策略是伪造它——为什么要把它全部记在内存中? 我不知道为什么说服我很重要,但我永远不会根据纸上的内容做出这样的决定。如果我的一个工程团队想花时间在这样的优化上,我的反应总是一样的:向我证明有一个问题需要解决。这总是通过编写代码的基准来完成的。由于shouldComponentUpdate可能导致性能问题而绕过框架和混淆代码是过早优化的缩影。关于问题的性质和解决方案可能存在的位置,存在太多假设。 【参考方案1】:

这可能是比您正在寻找的更普遍的答案,但从广义上讲:

    Redux 文档的建议是将 React 组件连接到组件层次结构中相当高的位置。 See this section.。这样可以使连接数保持可控,然后您可以将更新后的 props 传递给子组件。 React 的部分功能和可扩展性来自于避免渲染不可见的组件。例如,在 React 中我们根本不渲染组件,而不是在 DOM 元素上设置 invisible 类。重新渲染未更改的组件也完全不成问题,因为虚拟 DOM 差异过程优化了低级 DOM 交互。

【讨论】:

1.在无限滚动中,React 不再管理 DOM 节点(因为性能是一个问题,尤其是在移动设备上)。这意味着,例如,如果用户喜欢某个帖子(在无限滚动中),则该帖子必须更新以显示该更改,因此它必须自己连接。 2. 同意。这不是质疑 React 的力量,而是质疑 Redux 的力量。 Backbone 也可以与 React 一起使用。 作为一个非常晚的更新:最小化连接的建议已经过时。当前的建议是连接 UI 中您认为有必要的任何位置,事实上,最优化的性能模式依赖于 许多 连接,尤其是对于列表。【参考方案2】:

这不是 Redux 固有的问题,恕我直言。

顺便说一句,不要尝试同时渲染 100k 个组件,您应该尝试使用类似 react-infinite 或类似的库来伪造它,并且只渲染您的可见(或接近)项目列表。即使你成功地渲染和更新了一个 100k 的列表,它仍然没有性能并且需要大量的内存。这里有一些LinkedIn advices

此 anwser 将考虑您仍在尝试在 DOM 中呈现 100k 可更新项,并且您不希望每次更改都调用 100k 侦听器 (store.subscribe())。


2 所学校

在以函数式方式开发 UI 应用时,您基本上有两种选择:

始终从最顶端渲染

它运作良好,但涉及更多样板。这不完全是建议的 Redux 方式,但可以实现,有一些 drawbacks。请注意,即使您设法拥有一个单一的 redux 连接,您仍然需要在许多地方调用很多 shouldComponentUpdate。如果您有无限的视图堆栈(例如递归),则必须将所有中间视图都渲染为虚拟 dom,并且将在其中许多视图上调用 shouldComponentUpdate。所以即使你只有一个连接,这也不是真的更有效。

如果你不打算使用 React 生命周期方法而只使用纯渲染函数,那么你可能应该考虑其他只专注于该工作的类似选项,例如 deku(可以与 Redux 一起使用)

根据我自己的经验,在较旧的移动设备(例如我的 Nexus4)上,使用 React 执行此操作的性能不够,尤其是当您将文本输入链接到原子状态时。

将数据连接到子组件

这是react-redux 使用connect 所建议的。因此,当状态发生变化并且它仅与更深层次的孩子相关时,您只需渲染该孩子,而不必每次都渲染***组件,如上下文提供程序(redux/intl/custom...)或主应用程序布局。您还避免在其他孩子上调用shouldComponentUpdate,因为它已经融入了侦听器。调用大量非常快速的侦听器可能比每次渲染中间反应组件更快,并且它还允许减少大量传递道具的样板,因此对我来说与 React 一起使用是有意义的。

还要注意,身份比较非常快,每次更改时您都可以轻松地进行很多比较。记住 Angular 的脏检查:有些人确实设法用它来构建真正的应用程序!而且身份比较要快得多。


了解您的问题

我不确定是否完全理解您的所有问题,但我知道您的视图中包含大约 10 万个项目,您想知道是否应该对所有这 10 万个项目使用 connect,因为每次更改都会调用 10 万个侦听器似乎很昂贵。

这个问题似乎是使用 UI 进行函数式编程的本质所固有的:列表已更新,因此您必须重新渲染列表,但不幸的是,它是一个很长的列表,而且似乎效率低下......使用 Backbone你可以破解一些东西来只渲染孩子。即使你使用 React 渲染那个孩子,你也会以一种命令的方式触发渲染,而不是仅仅声明“当列表发生变化时,重新渲染它”。


解决您的问题

显然,连接 10 万个列表项似乎很方便,但由于调用了 10 万个 react-redux 侦听器,因此性能不佳,即使它们很快。

现在,如果您连接 100k 个项目的大列表而不是单独连接每个项目,您只需调用一个 react-redux 侦听器,然后必须以有效的方式呈现该列表。


天真的解决方案

迭代这 100k 个项目来渲染它们,导致 99999 个项目在 shouldComponentUpdate 中返回 false 和一个重新渲染:

list.map(item => this.renderItem(item))

高效解决方案一:自定义connect + 商店增强器

React-Redux 的 connect 方法只是一个 Higher-Order Component (HOC),它将数据注入到包装的组件中。为此,它会为每个连接的组件注册一个store.subscribe(...) 侦听器。

如果您想连接单个列表的 100k 项,这是您的应用程序值得优化的关键路径。您可以构建自己的,而不是使用默认的 connect

    存储增强器

公开一个额外的方法store.subscribeItem(itemId,listener)

包装 dispatch 以便每当调度与某个项目相关的操作时,您都会调用该项目的已注册侦听器。

redux-batched-subscribe 是此实现的一个很好的灵感来源。

    自定义连接

使用如下 API 创建一个高阶组件:

Item = connectItem(Item)

HOC 可以期待一个itemId 属性。它可以使用 React 上下文中的 Redux 增强存储,然后注册其侦听器:store.subscribeItem(itemId,callback)。原connect的源码可以作为基础灵感。

    HOC 只会在项目发生变化时触发重新渲染

相关回答:https://***.com/a/34991164/82609

相关的 react-redux 问题:https://github.com/rackt/react-redux/issues/269

高性能解决方案 2:监听子组件内的事件

也可以直接在组件中监听 Redux 动作,使用 redux-dispatch-subscribe 或类似的东西,这样在第一次列表渲染之后,你可以直接监听到 item 组件的更新并覆盖父列表的原始数据.

class MyItemComponent extends Component 
  state = 
    itemUpdated: undefined, // Will store the local
  ;
  componentDidMount() 
    this.unsubscribe = this.props.store.addDispatchListener(action => 
      const isItemUpdate = action.type === "MY_ITEM_UPDATED" && action.payload.item.id === this.props.itemId;
      if (isItemUpdate) 
        this.setState(itemUpdated: action.payload.item)
      
    )
  
  componentWillUnmount() 
    this.unsubscribe();
  
  render() 
    // Initially use the data provided by the parent, but once it's updated by some event, use the updated data
    const item = this.state.itemUpdated || this.props.item;
    return (
      <div>
        ...
      </div>
    );
  

在这种情况下,redux-dispatch-subscribe 的性能可能不是很好,因为您仍会创建 10 万个订阅。您宁愿使用类似store.listenForItemChanges(itemId) 的 API 构建自己的优化中间件,类似于 redux-dispatch-subscribe,将项目侦听器存储为映射,以便快速查找要运行的正确侦听器...


高效解决方案 3:向量尝试

更高效的方法是考虑使用像 vector trie 这样的持久数据结构:

如果您将 100k 项列表表示为 trie,则每个中间节点都有可能更快地短路渲染,这可以避免子节点中出现大量 shouldComponentUpdate

这个技术可以和ImmutableJS一起使用,你可以找到我用ImmutableJS做的一些实验:React performance: rendering big list with PureRenderMixin 但是它也有缺点,因为像 ImmutableJs 这样的库还没有公开公共/稳定 API 来做到这一点(issue),而且我的解决方案用一些无用的中间 &lt;span&gt; 节点(issue)污染了 DOM。

这是一个JsFiddle,它演示了如何有效地呈现一个包含 100k 个项目的 ImmutableJS 列表。初始渲染很长(但我猜你不会用 100k 项初始化你的应用程序!)但之后你会注意到每次更新只会导致少量shouldComponentUpdate。在我的示例中,我每秒只更新第一项,您注意到即使列表有 100k 项,它也只需要像 110 次调用 shouldComponentUpdate 这样更容易接受! :)

编辑:似乎 ImmutableJS 在某些操作上保留其不可变结构并不是那么好,例如在随机索引处插入/删除项目。这是一个JsFiddle,它演示了根据列表中的操作可以预期的性能。令人惊讶的是,如果你想在一个大列表的末尾追加许多项目,多次调用list.push(value) 似乎比调用list.concat(values) 保留了更多的树结构。

顺便说一句,据记载,List 在修改边缘时是有效的。我不认为在给定索引处添加/删除这些糟糕的表现与我的技术有关,而是与底层的 ImmutableJs List 实现有关。

列表实现双端队列,在末尾(push、pop)和开头(unshift、shift)都有效地添加和删除。

【讨论】:

当只有根节点被连接时,它必须确定应该更新哪个叶节点。这充其量是O(log(N)),并且对于每个内部节点,至少需要多1 个中间shouldComponentUpdate。如果没有更新叶节点,但添加了数据,这仍然会调用 O(N) shouldComponentUpdate 检查每个帖子的数据是否已更改(因为保存数据的对象已被修改)。如果无限滚动器在重新渲染期间卸载 React 将删除它们的节点,那么拥有 N 个连接的组件似乎仍然更快。 加勒特 我理解您的担忧。我添加了一个新的 JsFiddle,它对基本的 ImmutableJS 操作采取措施。如您所见,列表开头和结尾处的操作以及随机索引处的更新(在无限滚动视图中更可能发生)相对较快O(log(N))O(N) 的不良表现仅在您尝试拼接列表或在随机索引处添加/删除时出现。但是在无限滚动而不是删除项目中,您可以简单地将它们更新为未定义,据我所知,您不太可能希望对该列表进行复杂的切片 另外值得考虑的是,在 DOM 中维护 10 万个元素的列表也很不高效。您应该考虑假装无穷大,并在元素离开视口时卸载它们。您只能从该列表中取出 100 个项目的一部分并直接渲染/连接它,而不是要渲染 100k 项,这是可以接受的。 我添加了另一个基于自定义 redux-connect 的解决方案 我很欣赏这个被冲洗掉的小提琴,但我不确定它是否完全适用于手头的问题。我已经用我当前的解决方案来伪造无穷大。考虑一个具有 3 个块 [0、1 和 2] 的块式滚动条。 0 和 1 是可见的,但是当用户接近块 1 的末尾时,可见块现在必须更改为 1 和 2,因此隐藏 0 并保持 1。在 React 中,我们根本不渲染 0,导致它分离。我们渲染 1 和 2,它们附加了 2。但是 1 呢?

以上是关于React-Redux 应用程序真的可以像 Backbone 一样扩展吗?即使重新选择。在移动的主要内容,如果未能解决你的问题,请参考以下文章

P22:优化-React-redux程序优化

刷新后 React-Redux 状态丢失

React-Redux自制简易贪吃蛇小游戏

React-Redux自制简易贪吃蛇小游戏

React-Redux自制简易贪吃蛇小游戏

React-Redux自制简易贪吃蛇小游戏