How to fast render >10000 items using React + Flux?
Asked Answered
Z

5

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;
Zubkoff answered 27/11, 2015 at 14:40 Comment(6)
I just suggest a case. In my real work, I have 300 rows, each row contains 15 checkboxZubkoff
Consider implementing shouldComponentUpdate for each componentAsterisk
@Asterisk 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.Clemmer
But @Asterisk 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.Clemmer
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.Olly
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.Zubkoff
Y
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.

Yenyenisei answered 13/2, 2016 at 14:44 Comment(1)
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.Zubkoff
T
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());
    }
    
Tenotomy answered 27/11, 2015 at 17:17 Comment(2)
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.Zubkoff
You could see if my new option 5 solves the problem in a less intrusive way.Tenotomy
Z
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.

Zubkoff answered 19/1, 2016 at 17:34 Comment(1)
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.Setsukosett
Y
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.

Yenyenisei answered 13/2, 2016 at 14:44 Comment(1)
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.Zubkoff
C
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>
  );
},
Clemmer answered 27/11, 2015 at 15:14 Comment(6)
I need toArray() because immutable.map return an immutable map. facebook.github.io/immutable-js/docs/#/Map/mapZubkoff
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.Clemmer
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.Zubkoff
@Zubkoff i believe days React can render any object which implements an iterator these days. So you may not need to convertAsterisk
@Asterisk Is there any link related to render object which implements an iterator?Zubkoff
@Zubkoff 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 facebook.github.io/react/blog/2015/03/10/react-v0.13.htmlAsterisk
I
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 -_-.

Inkwell answered 13/12, 2015 at 16:1 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.