Improving AngularJS long list rendering performance using ReactJS
Development | Tihomir Kit

Improving AngularJS long list rendering performance using ReactJS

Friday, Jun 20, 2014 • 5 min read
If you're reading this, you most probably tried to make a long and/or complex list of items using a ng-repeat directive, and after seeing how it performs in terms of speed, felt like AngularJS failed you.

If you’re reading this, you most probably tried to make a long and/or complex list of items using a ng-repeat directive, and after seeing how it performs in terms of speed, felt like AngularJS failed you.

In some cases you might be able to avoid this problem by using paging or infinite scrolling, but sometimes that’s just not good enough. For example, lists that are displaying only a few items at a time (like any kind of a log) would simply be very impractical from the users point of view. Another reason - you might be using libraries like SlyJS that load a whole list at once to calculate the size of the embedded scrollbar and to make list swiping and elastic bounds work properly. Ooooor - your client simply demands it. :)

There are two problems with generating long lists in Angular:

  1. slow rendering speed (always)
  2. huge amount of watchers (if your list contains multiple interactive elements or lots of dynamic data that changes over time)

Slow rendering will make it longer for the data to actually show up in the browser (on initial and every subsequent render) and huge amount of watchers will make your website laggy and less responsive in general once everything is rendered. Keep in mind that recommended amount of watchers per page is around 2000. So if your list is not that long but “only” has a lot of watchers, you might be able to work around the problem using bindonce which will significantly decrease the number of watchers on your page. If your list however is quite long, rendering it will take some time.

In this post we’ll deal with improving the rendering speed problem. To accomplish this, we will use Facebook’s ReactJS library and ngReact directive as its wrapper. ReactJS not only renders DOM very fast initially, but it updates the parts that need updating even faster thanks to its virtual DOM diff implementation. This means ReactJS won’t re-render the whole list from scratch once it has already been rendered but it will keep track of rendered DOM elements internally and upon new invocation create new virtual DOM, compare it to the previous one and only apply the changes.

When using Angular, we usually want not to stray away too much from the “Angular way” because we don’t want to lose all the goodies that come with Angular. That’s where the ngReact directive jumps in as it wraps ReactJS rendering functionality into an easy-to-use angular directive. ngReact accepts the list we want to iterate upon and gives us a nice way to bind events to Angular scope which is pure awesomeness. Further down this post the whole workflow of using ReactJS within the Angular context will be explained. For the purpose of this post, I also created a fully functional demo which you can download and try out. The demo can render a list of items using both the standard ng-repeat way and the ReactJS way. It also allows you to change the number of items rendered, to interact with the list by expanding and updating lists nested items, count the watchers and it tracks the time needed to render the list (through the console).

Setup

To implement ReactJS and ngReact into an Angular app, you will need the following:

  1. An AngularJS app with a controller and a view template
  2. NodeJS installed on your machine (needed because of npm)
  3. react-tools npm package installed using npm install -g react-tools through your terminal
  4. ngReact downloaded and added to your project Scripts directory

React-tools will provide you with JSX which is a Reacts compiler that transforms its JS/XML-like syntax into native JavaScript. To write ReactJS components you will need an empty JS file with the following line at the beginning of the file /** @jsx React.DOM */ and any ReactJS components code bellow that. After you run the JSX watcher (jsx --watch jsx_folder/ scripts_folder/) on folder where that JS file resides in it will look for changes and (re)create a second (native JS) file which you will need to include into your project.

Code

/** @jsx React.DOM */
 
/*
* CLICK HANDLERS
**/
function createShowSubItemClickHandler(reactComponent) {
    var scope = reactComponent.props.scope;
    return scope.$apply.bind(
        scope,
        scope.onReactShowSubItemClick.bind(null, reactComponent)
    );
}
 
function createUpdateSubItemClickHandler(reactComponent) {
    var scope = reactComponent.props.scope;
    return scope.$apply.bind(
        scope,
        scope.onReactUpdateSubItemClick.bind(null, reactComponent)
    );
}
 
/*
* REACT COMPONENTS
**/
window.ReactSubItem = React.createClass({
    render: function() {
        var reactComponent = this.props.reactComponent;
        var subItem = reactComponent.props.item.prop6;
        var updateSubItemClickHandler = createUpdateSubItemClickHandler(reactComponent);
         
        return (
            <li>
                <ul>
                    <li>{subItem.text} {subItem.counter}</li>
                    <li><a href="javascript:void(0)" onClick={updateSubItemClickHandler}>Update</a></li>
                </ul>
            </li>
        );
    }
});
 
window.ReactItem = React.createClass({
    render: function() {
        this.props.startTime = new Date().getTime();
        var item = this.props.item;
        var reactSubItem = {
            reactComponent: this,
            scope: this.props.scope,
            item: item
        };
 
        var showSubItemClickHandler = createShowSubItemClickHandler(this);
 
        var subItem = null;
        if (item.prop6.show) {
            subItem = (
                <ReactSubItem reactComponent={this} />
            );
        }
 
        return (
            <ul>
                <li>{item.prop1}</li>
                <li>{item.prop2}</li>
                <li>{item.prop3}</li>
                <li>{item.prop4}</li>
                <li className="random-number">{item.prop5}</li>
                <li><a href="javascript:void(0)" onClick={showSubItemClickHandler}>{item.prop6.showHide} SubItem</a></li>
                {subItem}
            </ul>
        );
    },
    componentDidUpdate: function () {
        var time = (new Date().getTime() - this.props.startTime) + " ms";
        this.props.scope.setUpdateTime(time);
        console.log("REACT - Item updated in: " + time);
    }
});
 
window.ReactItemList = React.createClass({
    render: function() {
        this.props.startTime = new Date().getTime();
        var scope = this.props.scope;
        var items = scope.items;
         
        var rows = _.map(items, function(item) {
            return (
                <ReactItem item={item} scope={scope} />
            );
        });
 
        return (
            <div>{rows}</div>
        );
    },
    componentDidMount: function () {
        measureTime(this.props, "REACT - List mounted in: ");
    },
    componentDidUpdate: function () {
        measureTime(this.props, "REACT - List updated in: ");
    }
});
 
function measureTime(props, message) {
    if (props.scope.showReact) {
        var time = (new Date().getTime() - props.startTime) + " ms";
        props.scope.setUpdateTime(time);
        console.log(message + time);
    }
}

This code generates an unordered list with two anchors (one visible, and one hidden) which can be interacted with. Things are a bit backwards when writing ReactJS code - the parent-most ReactJS component (ReactItemList) is at the bottom of the JSX file and the nested components (ReactItem and ReactSubItem) are above it. This is simply because all the nested components need to be available in the code before their parent components. At the top of the file we have two click handlers and the neat thing about all this is that we have Angular scope inside our ReactJS code. This means we can call any exposed (scoped through the controller) variables and functions which gives us a lot of flexibility. You can notice how we propagated the scope all the way through the most components. We have used these two click handlers to make a connection between ReactJS onClick property and the two Angular scoped functions from the controller that look like this:

$scope.onReactShowSubItemClick = function (reactComponent) {
    var item = reactComponent.props.item;
    item.prop6.show = !item.prop6.show;
    item.prop6.showHide = item.prop6.show ? "Hide" : "Show";
    reactComponent.setState({ item: item, scope: $scope });
};
 
$scope.onReactUpdateSubItemClick = function (reactComponent) {
    var item = reactComponent.props.item;
    item.prop6.counter++;
    reactComponent.setState({ item: item, scope: $scope });
};

The neat thing about these two Angular functions is that they use React’s “setState” components directly. This gives us the ability to manipulate the row we interacted with directly. Upon clicking on an anchor, only the row it belongs to will be updated (through Reacts virtual DOM diff). This improves the rendering performance even further as now there is no need to compare the diff of the whole list but only of a single row.

Adding ngReact directive into your template is quite easy after we have everything else already prepared:

<div ng-react-component="ReactItemList" data="items" ></div>

ReactItemList is the name of the component we wish to use, and items is a scoped array defined in the controller.

Performance

For 2000 rows, demo app performance results look like this:

  • ng-repeat: Initial rendering: ~1750ms, Update: ~160ms, Watchers: 14009
  • ReactJS: Initial rendering: ~230ms, Update: ~2ms, Watchers: 9

Conclusion

Everything feels much more snappy and faster with ReactJS. This comes with a price of course: more code, somewhat clunky syntax and a bit more effort on our side to handle the “get back into Angular” from React part but it’s not that bad considering the performance benefits gained.

This should be enough to get you started, hope it helped.

Comments? Drop them bellow. Cheers!

Additional reads: