比如说 Backbone,React-Redux 应用程序真的可以扩展吗?即使重新选择。在移动

IT技术 performance backbone.js mobile reactjs redux
2021-04-21 15:49:39

在 Redux 中,对 store 的每次更改都会触发notify所有连接组件上的 a。这使开发人员的工作变得非常简单,但是如果您有一个包含 N 个连接组件的应用程序,并且 N 非常大怎么办?

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

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

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

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

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

2个回答

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

顺便说一句,与其尝试同时渲染 100k 个组件,您应该尝试使用像react-infinite或类似的东西来伪造它,并且只渲染列表中可见(或接近)的项目。即使您成功渲染和更新 100k 列表,它仍然没有性能,并且需要大量内存。以下是一些LinkedIn 的建议

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


2所学校

在以功能方式开发 UI 应用程序时,您基本上有两种选择:

始终从顶部渲染

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

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

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

将数据连接到子组件

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

另请注意,身份比较非常快,您可以在每次更改时轻松完成其中的很多工作。记住 Angular 的脏检查:有些人确实设法用它构建了真正的应用程序!身份比较要快得多。


了解您的问题

我不确定是否完全理解您的所有问题,但我知道您的视图中包含 100k 项,并且您想知道是否应该使用connect所有这些 100k 项,因为在每次更改时调用 100k 侦听器似乎代价高昂。

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


解决您的问题

显然,连接 100k 列表项看起来很方便,但由于调用了 100k react-redux 侦听器,因此性能不佳,即使它们速度很快。

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


天真的解决方案

迭代 100k 个项目来渲染它们,导致 99999 个项目返回 falseshouldComponentUpdate并重新渲染单个项目

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

高性能解决方案 1:自定义connect+ 商店增强器

connect阵营-终极版的方法仅仅是一个高次成分(HOC),该喷射数据到缠绕组件。为此,它store.subscribe(...)为每个连接的组件注册一个侦听器。

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

  1. 商店增强剂

暴露一个额外的方法 store.subscribeItem(itemId,listener)

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

这个实现的一个很好的灵感来源可以是redux-batched-subscribe

  1. 自定义连接

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

Item = connectItem(Item)

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

  1. HOC 只会在 item 发生变化时触发重新渲染

相关答案:https : //stackoverflow.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 万个订阅。您宁愿构建自己的优化中间件,类似于redux-dispatch-subscribeAPI 之类的store.listenForItemChanges(itemId),将项目侦听器存储为映射,以便快速查找要运行的正确侦听器...


高性能解决方案 3:向量尝试

一种更高效的方法会考虑使用像向量树这样的持久数据结构

特里

如果您将 100k 项列表表示为 trie,则每个中间节点都有可能更快地使渲染短路,从而可以避免大量shouldComponentUpdatein child。

这种技术可以与ImmutableJS一起使用,你可以找到我用 ImmutableJS 做的一些实验:react性能:用 PureRenderMixin 渲染大列表 它有缺点,但是像 ImmutableJs 这样的库还没有公开公共/稳定 API 来做到这一点(问题),我的解决方案使用一些无用的中间<span>节点(issue污染了 DOM

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

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

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

列表实现了 Deque,在末尾(push、pop)和开头(unshift、shift)都进行了有效的添加和删除。

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

这可能是一个比您正在寻找的更笼统的答案,但从广义上讲:

  1. Redux 文档中的建议是连接组件层次结构中相当高的 React 组件。请参阅本节。. 这使连接数保持可控,然后您可以将更新的 props 传递到子组件中。
  2. React 的部分功能和可扩展性来自避免渲染不可见组件。例如invisible,在 React 中我们根本不渲染组件,而不是在 DOM 元素上设置类。未更改的组件的重新渲染也完全不是问题,因为虚拟 DOM 差异过程优化了低级 DOM 交互。
作为一个非常晚的更新:最小化连接的建议已经过时。当前的建议是在您认为必要的 UI 中的任何位置进行连接,事实上,最优化的性能模式依赖于许多连接,尤其是对于列表。
2021-05-24 15:49:39
1. 在无限滚动中,React 不再管理 DOM 节点(因为性能是一个问题,尤其是在移动设备上)。这意味着,例如,如果用户喜欢一个帖子(在无尽的滚动条中),帖子必须更新以显示该更改,因此它必须自己连接。2. 同意。这不是质疑 React 的强大,而是质疑 Redux 的强大。Backbone 也可以与 React 一起使用。
2021-06-21 15:49:39