53

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?

Garrett
  • 11,451
  • 19
  • 85
  • 126
  • 1
    Assuming the number of components you `connect` is way less than N, this seems like largely a React question. If `shouldComponentUpdate` is `false`, the entire subtree of a component will not rerender, so that will help. It sounds like you're going to have a really large component tree still, so you'll probably want to do some advanced stuff with dynamically changing which components are mounted. Perhaps invest upfront time in mocking to see where the strain begins to show and test strategies from there. – acjay Jan 14 '16 at 06:00
  • Right. The N you're most concerned with is the number of **top-level** trees. If those top-level trees return false for shouldComponentUpdate, the entire sub tree is not examined. – Adam Rackis Jan 14 '16 at 06:06
  • @acjay in an endless scroller (at least my implementation), React is not involved, and the nodes are managed as separate containers. If there is an equally performant way to manage an endless scroller, I'm all ears, but in my searches I've found none that perform nearly as well as my raw JS solution. Our non-scroller components (ie. ` > > `) manage their performance well and are a much smaller problem. The performance hit I'm afraid of is when 100s of endless scroller containers must be attached (when the user is scrolling through feeds). – Garrett Jan 14 '16 at 07:18
  • To rephrase what acjay and Adam have already said from a different light, if you're connecting an arbitrarily large number of components to your store, you're not taking advantage of the best features of React. From the [React blog](https://facebook.github.io/react/blog/2015/10/07/react-v0.14.html#stateless-functional-components): In idiomatic React code, most of the components you write will be stateless. Why would your scroller be connected to the store more than once, regardless of the number of components within it? – Josh David Miller Jan 14 '16 at 07:19
  • @JoshDavidMiller For a performant endless scroller on mobile, React is not used. I use vanilla js to manage the DOM nodes, detaching blocks of connected containers that are no longer in view and attach blocks that are in view to save precious memory and prevent crashing (Samsung S3 is popular and limiting). If the entire scroller were connected to the store, React would rerender every time the visible block of posts is changed, which is a lot heavier than a single `node.removeChild`. Because performance is extremely limiting on old devices, vanilla seems to be the only way. – Garrett Jan 14 '16 at 07:35
  • 3
    Internally, React *will not* rerender the whole tree if the posts change. It will diff the DOM and render only what it needs to. Additionally, you can use `shouldComponentUpdate` to prevent it for edge cases. But assuming you've cut out React in this part of your code, it makes your question unfair: it's not "will Redux scale well", but rather "if I use React and Redux in ways in which it is not designed that create a lot of extra connections, would it scale well"? Also, a common tactic in infinite scroll is to fake it - why keep it all in memory? – Josh David Miller Jan 14 '16 at 07:45
  • I also suggest to take a look on [reselect](https://github.com/rackt/reselect) to cache your selectors, that could help a lot, if you have to pass the same state to a lot of components that would be calculated only once, until it change (assuming you're using immutable data). – Ingro Jan 14 '16 at 11:56
  • @JoshDavidMiller computing the virtual dom is heavy, and faking it is not simple. If there is a performant endless scroller solution in React, I'm all ears, but frankly I don't believe React was built for that kind of raw performance required to build a near-native experience on mobile devices (Facebook and LinkedIn are two that gave up, see https://www.facebook.com/notes/facebook-engineering/under-the-hood-rebuilding-facebook-for-ios/10151036091753920/ and http://venturebeat.com/2013/04/17/linkedin-mobile-web-breakup/). React was built for efficient rerendering, not infinite rendering. – Garrett Jan 14 '16 at 19:42
  • @Ingro already using it (as mentioned in the q) – Garrett Jan 14 '16 at 19:42
  • @Garrett I cannot speak to the performance as that is an empirical question and not one that can be resolved *a priori* on SO. I simply corrected a factual inaccuracy in your comment about the React rendering lifecycle and noted that your question is unfair at best. Put another way: you can't circumvent a library and then say it is a framework problem that it doesn't work as well anymore. – Josh David Miller Jan 14 '16 at 19:49
  • @JoshDavidMiller Agreed. But in this case, it seems necessary to circumvent React. After flushing out a solution on paper, consider the following setup: ` -> -> `. `` listens to scrolling and updates `visibleBlocks` when necessary. Every time `visibleBlocks` changes, `shouldComponentUpdate` compares every `post` in the visible ``s, `O(M)` where `M` is the number of posts per block. Circumventing React, vanilla js would simply remove the hidden blocks and add the new blocks, avoiding the unnecessary `shouldComponentUpdate`. – Garrett Jan 14 '16 at 20:26
  • 1
    I'm not sure why it's important to convince me, but I would never make a decision like that based on what's on paper. If one of my engineering teams wanted to spend time on such an optimization, my response would always be the same: prove to me that there's a problem to solve. That's always done with benchmarks of written code. Working around frameworks and obfuscating code because `shouldComponentUpdate` *might* cause performance problems is the epitome of premature optimization. There are just too many assumptions, both on the nature of the problem and where the solution may lie. – Josh David Miller Jan 14 '16 at 21:12
  • Maybe you haven't worked with Android before, but there is no such thing as premature optimization. When a single `console.info` can take an entire millisecond, nothing is not worth optimizing on Android. Of course, you can prioritize, and yes, if you have dollars to waste on an engineering team to run benchmarks that's great. But in most high-paced tech companies, you need to go with your gut and hope for the best. My gut says that trying to beat the raw performance of vanilla just to fit React into a solution is naïve. Sometimes, good ol' bits n bytes are the best way to go. – Garrett Jan 15 '16 at 00:00
  • @Garrett I sympathize with your concerns, even though I use React/Redux I'm not sure it's ideal for every use case. But regarding "the number of connected components can get very large": do you mean you render each post in its own `react-redux` `connect()` container? I would try to implement the posts view with a single `connect()` container at the top. Since each container has to respond to state changes, I'm sure gaearon didn't design `react-redux` to be used with thousands of containers. – Andy Oct 03 '16 at 15:07
  • @Andy: on the contrary - benchmarks have shown that _more_ connections are generally better. The cost of running O(N) subscriptions and `mapState` checks (with many that bail out) is faster than fully re-rendering the entire VDOM. See http://somebody32.github.io/high-performance-redux/ for examples . The new React-Redux v5 implementation works even better for those scenarios, as well. – markerikson Oct 04 '16 at 02:09
  • @markerikson weird. Ironic how that defies one of the long standing principles of React usage, that one should typically have a single view container that renders a bunch of dump components... – Andy Oct 04 '16 at 17:05
  • But I just want to point out that it's O(N) `mapState` checks or `shouldComponentUpdate` checks when a single list item is selected or deselected...whereas with a signals/slots architecture like Qt has, only O(1) work would be done when a single list item is selected or deselected. They both have their strengths and weaknesses. – Andy Oct 04 '16 at 17:07

2 Answers2

91

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:

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).

Sebastien Lorber
  • 89,644
  • 67
  • 288
  • 419
  • When only the **root node** is connected, it must figure out which **leaf node(s)** should get updated. This is, at best, `O(log(N))`, and requires at least 1 more intermediate `shouldComponentUpdate` for every **internal node**. If no leaf nodes are updated, but data is added, this would still invoke `O(N)` `shouldComponentUpdate` checks to see if the data for every post has changed (since the object holding the data has been modified). If the endless scroller unmounts nodes where React would remove them during a rerender, having N connected components still seems faster. – Garrett Jan 14 '16 at 20:06
  • Garrett I understand your concerns. I've added a new JsFiddle that takes measures on basic ImmutableJS operations. As you can see, operations at the begin and end of the list, as well as updates at a random index (which are much more likely to happen in an infinite scroll view) are relatively fast `O(log(N))`. The bad performances `O(N)` only arise when you try to splice the list or add/remove at a random index. But in an infinite scroll instead of removing items you could simply update them to undefined, and it is unlikely you would like to do complex slicings of that list as far as I know – Sebastien Lorber Jan 15 '16 at 11:28
  • Also it's worth considering that it's also not efficient to maintain a list of 100k elements in the DOM. You should consider faking the infinity instead and unmounting the elements as they leave the viewport. Instead of having 100k items to render, you could only take a slice of 100 items of that list and render/connect it directly which would be acceptable. – Sebastien Lorber Jan 15 '16 at 11:52
  • I have added another solution based on a custom redux-connect – Sebastien Lorber Jan 15 '16 at 22:25
  • I appreciate the flushed-out fiddle, but I'm not sure if it's totally applicable to the problem at hand. I am already faking the infinity with my current solution. Consider a block-style scroller with 3 blocks, [0, 1, and 2]. 0 and 1 are visible, but as the user is nearing the end of block 1, the visible blocks must now change to 1 and 2, therefore hiding 0 and keeping 1. In React, we simply do not render 0, causing it to be detached. We render 1 and 2, which attaches 2. But what about 1? – Garrett Jan 15 '16 at 22:53
  • If we're using 1 container that splits the data into blocks, then a `===` doesn't work since new data was added (2) and `data = { ...block1, ...block2, ...block3 }` is a different object. There are 2 options, a deep comparison with `data` and `nextProps.data` or a rerender. Both are expensive, and both are avoided in my solution where each element in each block is connected and the infinite scroller is managed by vanilla js which mounts/unmounts as needed. This leads back to the original problem, where this could still require M connected components per view. – Garrett Jan 15 '16 at 22:57
  • sorry but it is hard to follow your problem. Can you edit your question and provide a minimal JsFiddle that explains the problem, or at least the shape of the datastructure you want to render? Because there is no concept of "attached/detached" in React, and also it's not clear what is a "block". It seems to me it's a container for some items, and then I'm not sure you are dealing with an infinite list of blocks or an infinite list of items inside a single block. – Sebastien Lorber Jan 15 '16 at 23:45
  • Every section of this answer deserves its own upvote. Thanks a bundle! – Craig Walker Apr 15 '16 at 17:01
  • @SebastienLorber I've encountered a bottleneck not mentioned in your answer: suppose the 100k items are added one by one because they're being delivered individually over a websocket (e.g. Meteor), but as fast as possible because they're initial data for a subscription. That's 100k state updates/rerenders. In that case some kind of batching is required. In general, high-frequency state updates are more challenging. I would say Redux/React *is* inherently more difficult to scale than a Backbone app. – Andy Sep 29 '16 at 18:58
  • @Andy I've read your github issue about that already. Sure you have to batch. Your application should only render once during bootstrap and it's your responsability to handle that. Scaling Redux is no more complicated than scaling Backbone. If you used immutable collections with backbone and mutate it on each meteor event, it would be the exact same problem. – Sebastien Lorber Sep 30 '16 at 08:34
  • @SebastienLorber Do you know anyone who uses immutable collections with Backbone? I don't think Immuable.js even existed back in Backbone's heyday. But in any case you're right, handling thousands of events at once would probably be challenging with Backbone as well. – Andy Sep 30 '16 at 17:41
  • @SebastienLorber So do you structure your apps such that some top-level component can determine when all subscriptions for the given route are ready and then render? Do you initially render a loading message? – Andy Sep 30 '16 at 17:45
  • @Andy some people are migrating progressively from Backbone to React and using ImmutableJS with both. I don't use Meteor or any realtime tech, and don't do server-side rendering either. It's usually adviced for good UX (progressive apps) to render first the app shell with loader, and then the content (unless that content needs to be indexed and initially served). I guess throttling the rendering with Meteor subscriptions can be an easy workaroud for your performance problems. I don't know enough Meteor to say more. – Sebastien Lorber Sep 30 '16 at 23:36
  • @SebastienLorber I was also thinking, even if one can do a single render on page load, later the user might navigate to a route that has to load thousands of documents, so some kind of throttling is necessary in any case. – Andy Oct 02 '16 at 05:21
  • It's easy for web devs to ignore the challenges of realtime or domains like CAD, Ableton Live, etc. – Andy Oct 02 '16 at 05:24
  • @Andy frameworks like Relay (or Dataloader: https://github.com/facebook/dataloader) can help to batch multiple requests into a single fat one. Like said in this answer, it's not really recommended to display thousands of documents into the same page without "virtualization": no need to load the data immediately if it won't be seen anytime soon in viewport. For real-time I've somehow my opinion on that and don't think Meteor is a good solution: http://stackoverflow.com/a/26633455/82609 Not sure to understand your point about CAD/music. – Sebastien Lorber Oct 02 '16 at 16:18
  • @SebastienLorber I'm not actually displaying the thousands of documents in my app; I'm basically using them in a client-side join. This is a case where the number of documents loaded is not directly related to the number of DOM elements displayed on the screen. Not that I think Meteor or Mongo are ideal for what I'm doing, but I'm stuck with them in my job. I might use Meatier if I had a choice, but I'm not 100% convinced by RethinkDb. I'm not sure what your link has to do with Meteor, it seems like you think Meteor is AJAX-based? But it's Websocket-based. – Andy Oct 03 '16 at 14:57
  • @SebastienLorber my point about CAD/music is I think they have such high CPU load and complex state that they would be very difficult to implement with React/Redux. – Andy Oct 03 '16 at 14:57
4

This may be a more general answer than you're looking for, but broadly speaking:

  1. The recommendation from the Redux docs is to connect React components fairly high in the component hierarchy. See this section.. This keeps the number of connections manageable, and you can then just pass updated props into the child components.
  2. Part of the power and scalability of React comes from avoiding rendering of invisible components. For example instead of setting an invisible class on a DOM element, in React we just don't render the component at all. Rerendering of components that haven't changed isn't a problem at all as well, since the virtual DOM diffing process optimizes the low level DOM interactions.
mjhm
  • 16,497
  • 10
  • 44
  • 55
  • 1. In an endless scroller, React is no longer managing the DOM nodes (because performance is an issue, especially on mobile). That means that, for example, if a user likes a post (in the endless scroller), the post must get updated to show that change, so it must be connected itself. 2. Agreed. This is not questioning the power of React, but the power of Redux. Backbone can be used with React as well. – Garrett Jan 14 '16 at 07:12
  • 6
    As a very belated update: the recommendation to minimize connections is outdated. The current advice is to connect anywhere in your UI that you feel necessary, and in fact, the most optimized performance patterns rely on _many_ connections, particularly for lists. – markerikson Oct 04 '16 at 02:06