6

I would like to ask what is the correct way to fast render > 10000 items in React.

Suppose I want to make a checkboxList which contain over dynamic 10000 checkbox items.

I make a store which contain all the items and it will be used as state of checkbox list.

When I click on any checkbox item, it will update the corresponding item by action and so the store is changed.

Since store is changed so it trigger the checkbox list update.

The checkbox list update its state and render again.

The problem here is if I click on any checkbox item, I have to wait > 3 seconds to see the checkbox is ticked. I don't expect this as only 1 checkbox item need to be re-rendered.

I try to find the root cause. The most time-consuming part is inside the checkbox list render method, related to .map which create the Checkbox component to form componentList.. But actually only 1 checkbox have to re-render.

The following is my codes. I use ReFlux for the flux architecture.

CheckboxListStore

The Store store all the checkbox item as map. (name as key, state (true/false) as value)

const Reflux = require('reflux');
const Immutable = require('immutable');
const checkboxListAction = require('./CheckboxListAction');

let storage = Immutable.OrderedMap();
const CheckboxListStore = Reflux.createStore({
 listenables: checkboxListAction,
 onCreate: function (name) {
  if (!storage.has(name)) {
   storage = storage.set(name, false);
   this.trigger(storage);
  }
 },
 onCheck: function (name) {
  if (storage.has(name)) {
   storage = storage.set(name, true);
   this.trigger(storage);
  }
 },
 onUncheck: function (name) {
  if (storage.has(name)) {
   storage = storage.set(name, false);
   this.trigger(storage);
  }
 },
 getStorage: function () {
  return storage;
 }
});

module.exports = CheckboxListStore;

CheckboxListAction

The action, create, check and uncheck any checkbox item with name provided.

const Reflux = require('reflux');
const CheckboxListAction = Reflux.createActions([
 'create',
 'check',
 'uncheck'
]);
module.exports = CheckboxListAction;

CheckboxList

const React = require('react');
const Reflux = require('reflux');
const $ = require('jquery');
const CheckboxItem = require('./CheckboxItem');
const checkboxListAction = require('./CheckboxListAction');
const checkboxListStore = require('./CheckboxListStore');
const CheckboxList = React.createClass({
 mixins: [Reflux.listenTo(checkboxListStore, 'onStoreChange')],
 getInitialState: function () {
  return {
   storage: checkboxListStore.getStorage()
  };
 },
 render: function () {
  const {storage} = this.state;
  const LiComponents = storage.map((state, name) => {
   return (
    <li key = {name}>
     <CheckboxItem name = {name} />
    </li>
   );
  }).toArray();
  return (
   <div className = 'checkbox-list'>
    <div>
     CheckBox List
    </div>
    <ul>
     {LiComponents}
    </ul>
   </div>
  );
 },
 onStoreChange: function (storage) {
  this.setState({storage: storage});
 }
});

module.exports = CheckboxList;

CheckboxItem Inside onChange callback, I call the action to update the item.

const React = require('react');
const Reflux = require('reflux');
const $ = require('jquery');
const checkboxListAction = require('./CheckboxListAction');
const checkboxListStore = require('./CheckboxListStore');

const CheckboxItem = React.createClass({
 mixins: [Reflux.listenTo(checkboxListStore, 'onStoreChange')],
 propTypes: {
  name: React.PropTypes.string.isRequired
 },
 getInitialState: function () {
  const {name} = this.props;
  return {
   checked: checkboxListStore.getStorage().get(name)
  };
 },
 onStoreChange: function (storage) {
  const {name} = this.props;
  this.setState({
   checked: storage.get(name)
  });
 },
 render: function () {
  const {name} = this.props;
  const {checked} = this.state;
  return (
   <div className = 'checkbox' style = {{background: checked ? 'green' : 'white'}} >
    <span>{name}</span>
    <input ref = 'checkboxElement' type = 'checkbox'
     onChange = {this.handleChange}
     checked = {checked}/>
   </div>
  );
 },
 handleChange: function () {
  const {name} = this.props;
  const checked = $(this.refs.checkboxElement).is(':checked');
  if (checked) {
   checkboxListAction.check(name);
  } else {
   checkboxListAction.uncheck(name);
  }
 }
});

module.exports = CheckboxItem;
RaymondFakeC
  • 75
  • 1
  • 7
  • I just suggest a case. In my real work, I have 300 rows, each row contains 15 checkbox – RaymondFakeC Nov 27 '15 at 14:45
  • Consider implementing `shouldComponentUpdate` for each component – WickyNilliams Nov 27 '15 at 15:20
  • @WickyNilliams if the performance bottleneck is inside the `.map` of the 10,000 items (not in the render apparently), then `shouldComponentUpdate` for each item is not likely to help much in terms of performance. – wintvelt Nov 27 '15 at 16:13
  • But @WickyNilliams may have a very good point: where is your performance bottleneck? a) in creating the array of 10000? Or b) in rendering the created array of 10000? You could do simple `console.log(Date.now())` before and after defining your LiComponents to check. – wintvelt Nov 27 '15 at 16:14
  • I ran into a similar problem before. The issue was that there were too many elements being displayed at one time. I solved it by hiding (using display: none or visibility: hidden) any elements that are outside the viewport. –  Nov 27 '15 at 20:24
  • I am thinking of this solution. 1. First, use AltJs instead of Reflux. 2. Wrap all checkbox with AltContainer to listen to store, check if it is self item, if yes, setState. 3. Since checkboxList doesn't related to the store.. it just print out all the checkbox. also wrap it as AltContainer to listen to store, but only render when store size got changed. – RaymondFakeC Nov 28 '15 at 06:34

5 Answers5

2

There are a few approaches you can take:

  1. Don't render all 10,000 - just render the visible check boxes (+ a few more) based on panel size and scroll position, and handle scroll events to update the visible subset (use component state for this, rather than flux). You'll need to handle the scroll bar in some way, either by rendering one manually, or easier by using the normal browser scroll bar by adding huge empty divs at the top and bottom to replace the checkboxes you aren't rendering, so that the scroll bar sits at the correct position. This approach allows you to handle 100,000 checkboxes or even a million, and the first render is fast as well as updates. Probably the preferred solution. There are lots of examples of this kind of approach here: http://react.rocks/tag/InfiniteScroll
  2. Micro-optimize - you could do storage.toArray().map(...) (so that you aren't creating an intermediate map), or even better, make and empty array and then do storage.forEach(...) adding the elements with push - much faster. But the React diffing algorithm is still going to have to diff 10000 elements, which is never going to be fast, however fast you make the code that generates the elements.
  3. Split your huge Map into chunks in some way, so that only 1 chunk changes when you check a chechbox. Also split up the React components in the same way (into CheckboxListChunks) or similar. This way, you'll only need to re-render the changed chunk, as long as you have a PureComponent type componentShouldUpdate function for each chunk (possibly Reflux does this for you?).
  4. Move away from ImmutableJS-based flux, so you have better control over what changes when (e.g. you don't have to update the parent checkbox map just because one of the children has changed).
  5. Add a custom shouldComponentUpdate to CheckboxList:

    shouldComponentUpdate:function(nextProps, nextState) {
        var storage = this.state.storage;
        var nextStorage = nextState.storage;
        if (storage.size !== nextStorage.size) return true;
        // check item names match for each index:
        return !storage.keySeq().equals(nextStorage.keySeq());
    }
    
TomW
  • 3,923
  • 1
  • 23
  • 26
  • 1. It is good solution for virtual scroll, but how about if I really want to show 1000 + checkboxes on the page. The checkbox size is just around 13px * 13px. 2. It doesn't help, as you say, the slowest part is virtual dom diff. 3. It is good solution and what I thought was quite similar. – RaymondFakeC Nov 28 '15 at 06:32
  • You could see if my new option 5 solves the problem in a less intrusive way. – TomW Nov 30 '15 at 17:54
1

Btw, I give up flux... I finally decided to use mobservable to solve my problem. I have made an example https://github.com/raymondsze/react-example see the https://github.com/raymondsze/react-example/tree/master/src/mobservable for the coding.

RaymondFakeC
  • 75
  • 1
  • 7
  • Hi! Could you please expand the content in your answer? Content from links can change or be deleted over time and your answer post will no longer make sense. – CCovey Jan 19 '16 at 18:06
1

Beyond the initial render, you can significantly increase rendering speed of large collections by using Mobservable. It avoids re-rendering the parent component that maps over the 10.000 items unnecessarily when a child changes by automatically applying side-ways loading. See this blog for an in-depth explanation.

mweststrate
  • 4,890
  • 1
  • 16
  • 26
  • I am using Mobservable to solve my problem if you see my answer. You created a nice library, my code is much simpler after applying Mobservable!! Anyway, I found that facebook released a library called relay, not sure it is used to solve the similar issue or not. – RaymondFakeC Feb 15 '16 at 04:40
0

Your render function looks somewhat more complicated then it needs to be:

  • it first generates an array of JSX components
  • then converts applies a (jQuery?) .toArray()
  • then returns this newly generated array.

Maybe simplifying your render function to something like this would help?

render: function () {
  return (
    <div className = 'checkbox-list'>
      <div>
        CheckBox List
      </div>
      <ul>
        {this.state.storage.map((state, name) => {
          return (
            <li key = {name}>
              <CheckboxItem name = {name} />
            </li>
          );
         })}
      </ul>
    </div>
  );
},
wintvelt
  • 13,855
  • 3
  • 38
  • 43
  • I need toArray() because immutable.map return an immutable map. https://facebook.github.io/immutable-js/docs/#/Map/map – RaymondFakeC Nov 27 '15 at 15:51
  • I do not see that you require or define any immutable objects in your code (but may have missed). Not sure if immutable.map will be very helpful in your case: if 1 of 10,000 items is different, your entire array will be different, and will be re-rendered (meaning react will check if each item needs to be re-rendered in DOM). And the immutable.map variant may even cause performance issues (10000 immutable checks needed?) I would think the standard javascript `.map()` is better in your case. – wintvelt Nov 27 '15 at 16:10
  • you are right, because I forgot to add the PureRenderMixin, you can see the state is Immutable object so I can use it to faster the shouldComponentUpdate part to do shadow diff instead of deep diff. But even i add it, it doesn't help so much. – RaymondFakeC Nov 28 '15 at 06:29
  • @RaymondFakeC i believe days React can render any object which implements an iterator these days. So you may not need to convert – WickyNilliams Nov 30 '15 at 10:48
  • @WickyNilliams Is there any link related to render object which implements an iterator? – RaymondFakeC Nov 30 '15 at 11:05
  • @RaymondFakeC it's mentioned in the release notes for 0.13: *"Support for iterators and immutable-js sequences as children"*. I should have been more clear that I meant children https://facebook.github.io/react/blog/2015/03/10/react-v0.13.html – WickyNilliams Dec 01 '15 at 13:22
0

Do you really need to save the check status in you store every time check/uncheck?

I recently meet similar problem like you. Just save a checkedList array [name1,name2 ...] in the CheckboxList component's state, and change this checkedList every time you check/uncheck an item. When you want to save check status to data storage, call an Action.save() and pass the checkedList to store.
But if you really need to save to data storage every time check/uncheck, this solution won't help -_-.

Zhang Chao
  • 757
  • 4
  • 14