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

标签 performance backbone.js mobile reactjs redux

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

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

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

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

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

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

最佳答案

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

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

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

2所学校

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

始终从顶部渲染

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

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

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

将数据连接到子组件

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

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

了解您的问题

我不确定是否完全理解您的所有问题,但我知道您的 View 中有 10 万个项目,您想知道是否应该使用 connect使用所有这些 100k 项,因为在每次更改时调用 100k 监听器似乎代价高昂。

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

解决您的问题

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

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

天真的解决方案

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

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

高性能解决方案 1:自定义 connect + 存储增强器
connect React-Redux 的方法只是一个 Higher-Order Component (HOC) 将数据注入(inject)到被包装的组件中。为此,它注册了一个 store.subscribe(...)每个连接组件的监听器。

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

  • 公开一个额外的方法 store.subscribeItem(itemId,listener)
    包裹 dispatch这样无论何时调度与项目相关的操作,您都可以调用该项目的注册监听器。

    此实现的一个很好的灵感来源可以是 redux-batched-subscribe .
  • 自定义连接

  • 使用如下 API 创建高阶组件:
    Item = connectItem(Item)
    

    HOC 可以期待 itemId属性(property)。它可以使用 React 上下文中的 Redux 增强存储,然后注册其监听器:store.subscribeItem(itemId,callback) .源代码原connect可以作为基础灵感。
  • HOC 只会在项目改变时触发重新渲染

  • 相关答案:https://stackoverflow.com/a/34991164/82609

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

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

    也可以直接在组件中监听 Redux Action ,使用 redux-dispatch-subscribe或类似的东西,以便在第一次列表呈现后,您可以直接在项目组件中监听更新并覆盖父列表的原始数据。
    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-subscribe使用类似 store.listenForItemChanges(itemId) 的 API ,将项目监听器存储为 map ,以便快速查找要运行的正确监听器...

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

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

    Trie

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

    此技术可用于 ImmutableJS你可以找到我用 ImmutableJS 做的一些实验:React performance: rendering big list with PureRenderMixin
    然而它有缺点,因为像 ImmutableJs 这样的库还没有公开公共(public)/稳定的 API 来做到这一点( issue ),我的解决方案用一些无用的中间体 <span> 污染了 DOM。节点( issue )。

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

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

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

    Lists implement Deque, with efficient addition and removal from both the end (push, pop) and beginning (unshift, shift).

    关于performance - 比如说 Backbone,React-Redux 应用程序真的可以扩展吗?即使重新选择。在移动,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/34782249/

    相关文章:

    c# - 用于移动设备的 Web - ASP.NET 的最佳实践

    java - C# 中子字符串的实现及其使用的算法是什么?

    javascript - Backbone.js - 一个 View 多个模型

    javascript - 以 Express 方式将主干集合保存到 MongoDB

    javascript - Facebook javascript api 和 iPad、Android

    iphone - Flurry Analytics 与移动平台上的 Google Analytics

    c++ - C++ 编译器可以优化对同一指针的重复虚函数调用吗?

    Python/C++快速高斯拟合4分

    java - 在数据库中排序数据的优缺点?

    javascript - 从与模型 ID 列表匹配的主干集合中提取多个模型