比如说 Backbone,React-Redux 应用程序真的可以扩展吗?即使重新选择.在移动 [英] Can a React-Redux app really scale as well as, say Backbone? Even with reselect. On mobile

查看:14
本文介绍了比如说 Backbone,React-Redux 应用程序真的可以扩展吗?即使重新选择.在移动的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

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

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

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

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

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

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

解决方案

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

顺便说一句,与其尝试同时渲染 100k 个组件,您应该尝试使用像

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

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

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

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

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

<块引用>

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

In Redux, every change to the store triggers a notify on all connected components. This makes things very simple for the developer, but what if you have an application with N connected components, and N is very large?

Every change to the store, even if unrelated to the component, still runs a shouldComponentUpdate with a simple === test on the reselected paths of the store. That's fast, right? Sure, maybe once. But N times, for every change? This fundamental change in design makes me question the true scalability of Redux.

As a further optimization, one can batch all notify calls using _.debounce. Even so, having N === tests for every store change and handling other logic, for example view logic, seems like a means to an end.

I'm working on a health & fitness social mobile-web hybrid application with millions of users and am transitioning from Backbone to Redux. In this application, a user is presented with a swipeable interface that allows them to navigate between different stacks of views, similar to Snapchat, except each stack has infinite depth. In the most popular type of view, an endless scroller efficiently handles the loading, rendering, attaching, and detaching of feed items, like a post. For an engaged user, it is not uncommon to scroll through hundreds or thousands of posts, then enter a user's feed, then another user's feed, etc. Even with heavy optimization, the number of connected components can get very large.

Now on the other hand, Backbone's design allows every view to listen precisely to the models that affect it, reducing N to a constant.

Am I missing something, or is Redux fundamentally flawed for a large app?

解决方案

This is not a problem inherent to Redux IMHO.

By the way, instead of trying to render 100k components at the same time, you should try to fake it with a lib like react-infinite or something similar, and only render the visible (or close to be) items of your list. Even if you succeed to render and update a 100k list, it's still not performant and it takes a lot of memory. Here are some LinkedIn advices

This anwser will consider that you still try to render 100k updatable items in your DOM, and that you don't want 100k listeners (store.subscribe()) to be called on every single change.


2 schools

When developing an UI app in a functional way, you basically have 2 choices:

Always render from the very top

It works well but involves more boilerplate. It's not exactly the suggested Redux way but is achievable, with some drawbacks. Notice that even if you manage to have a single redux connection, you still have have to call a lot of shouldComponentUpdate in many places. If you have an infinite stack of views (like a recursion), you will have to render as virtual dom all the intermediate views as well and shouldComponentUpdate will be called on many of them. So this is not really more efficient even if you have a single connect.

If you don't plan to use the React lifecycle methods but only use pure render functions, then you should probably consider other similar options that will only focus on that job, like deku (which can be used with Redux)

In my own experience doing so with React is not performant enough on older mobile devices (like my Nexus4), particularly if you link text inputs to your atom state.

Connecting data to child components

This is what react-redux suggests by using connect. So when the state change and it's only related to a deeper child, you only render that child and do not have to render top-level components everytime like the context providers (redux/intl/custom...) nor the main app layout. You also avoid calling shouldComponentUpdate on other childs because it's already baked into the listener. Calling a lot of very fast listeners is probably faster than rendering everytime intermediate react components, and it also permits to reduce a lot of props-passing boilerplate so for me it makes sense when used with React.

Also notice that identity comparison is very fast and you can do a lot of them easily on every change. Remember Angular's dirty checking: some people did manage to build real apps with that! And identity comparison is much faster.


Understanding your problem

I'm not sure to understand all your problem perfectly but I understand that you have views with like 100k items in it and you wonder if you should use connect with all those 100k items because calling 100k listeners on every single change seems costly.

This problem seems inherent to the nature of doing functional programming with the UI: the list was updated, so you have to re-render the list, but unfortunatly it is a very long list and it seems unefficient... With Backbone you could hack something to only render the child. Even if you render that child with React you would trigger the rendering in an imperative way instead of just declaring "when the list changes, re-render it".


Solving your problem

Obviously connecting the 100k list items seems convenient but is not performant because of calling 100k react-redux listeners, even if they are fast.

Now if you connect the big list of 100k items instead of each items individually, you only call a single react-redux listener, and then have to render that list in an efficient way.


Naive solution

Iterating over the 100k items to render them, leading to 99999 items returning false in shouldComponentUpdate and a single one re-rendering:

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


Performant solution 1: custom connect + store enhancer

The connect method of React-Redux is just a Higher-Order Component (HOC) that injects the data into the wrapped component. To do so, it registers a store.subscribe(...) listener for every connected component.

If you want to connect 100k items of a single list, it is a critical path of your app that is worth optimizing. Instead of using the default connect you could build your own one.

  1. Store enhancer

Expose an additional method store.subscribeItem(itemId,listener)

Wrap dispatch so that whenever an action related to an item is dispatched, you call the registered listener(s) of that item.

A good source of inspiration for this implementation can be redux-batched-subscribe.

  1. Custom connect

Create a Higher-Order component with an API like:

Item = connectItem(Item)

The HOC can expect an itemId property. It can use the Redux enhanced store from the React context and then register its listener: store.subscribeItem(itemId,callback). The source code of the original connect can serve as base inspiration.

  1. The HOC will only trigger a re-rendering if the item changes

Related answer: https://stackoverflow.com/a/34991164/82609

Related react-redux issue: https://github.com/rackt/react-redux/issues/269

Performant solution 2: listening for events inside child components

It can also be possible to listen to Redux actions directly in components, using redux-dispatch-subscribe or something similar, so that after first list render, you listen for updates directly into the item component and override the original data of the parent list.

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>
    );
  }
}

In this case redux-dispatch-subscribe may not be very performant as you would still create 100k subscriptions. You'd rather build your own optimized middleware similar to redux-dispatch-subscribe with an API like store.listenForItemChanges(itemId), storing the item listeners as a map for fast lookup of the correct listeners to run...


Performant solution 3: vector tries

A more performant approach would consider using a persistent data structure like a vector trie:

If you represent your 100k items list as a trie, each intermediate node has the possibility to short-circuit the rendering sooner, which permits to avoid a lot of shouldComponentUpdate in childs.

This technique can be used with ImmutableJS and you can find some experiments I did with ImmutableJS: React performance: rendering big list with PureRenderMixin It has drawbacks however as the libs like ImmutableJs do not yet expose public/stable APIs to do that (issue), and my solution pollutes the DOM with some useless intermediate <span> nodes (issue).

Here is a JsFiddle that demonstrates how a ImmutableJS list of 100k items can be rendered efficiently. The initial rendering is quite long (but I guess you don't initialize your app with 100k items!) but after you can notice that each update only lead to a small amount of shouldComponentUpdate. In my example I only update the first item every second, and you notice even if the list has 100k items, it only requires something like 110 calls to shouldComponentUpdate which is much more acceptable! :)

Edit: it seems ImmutableJS is not so great to preserve its immutable structure on some operations, like inserting/deleting items at a random index. Here is a JsFiddle that demonstrates the performance you can expect according to the operation on the list. Surprisingly, if you want to append many items at the end of a large list, calling list.push(value) many times seems to preserve much more the tree structure than calling list.concat(values).

By the way, it is documented that the List is efficient when modifying the edges. I don't think these bad performances on adding/removing at a given index are related to my technique but rather related to the underlying ImmutableJs List implementation.

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

这篇关于比如说 Backbone,React-Redux 应用程序真的可以扩展吗?即使重新选择.在移动的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆