JavaScript improve ng-repeat (read only html table) rendering performance in Angular

I've read half a dozen articles on improving performance with ng-repeat and so far I can't find a straight forward way at improving rendering of a simple bind once table.

I've profiled various approaches and so far the best I could do was 5 seconds of render time for ~4400 rows.

** a note about this test case. I am using a larger dataset to push performance testing to a challenging dataset. Normally the table would be 1000 rows or less. I'd like to avoid paging as in many cases seeing all data is useful for scanning for anomalies. More importantly the rendering time of 300 rows is also not acceptable to me as it freezes the browser for a short period of time, and that's what i'd like to eliminate. I am fully aware that rendering less data will be faster, I am looking at maximizing performance of larger datasets.

My initial approach

<tbody>
    <tr ng-repeat="f in vm.rowDetails">
        <td class="checkbox-column"><input type="checkbox" ng-model="f.selected" /></td>
        <td class="w100">{{f.Code}}</td>
        <td class="w50">{{f.Class}}</td>
        <td>{{f.WebName}}</td>
        <td>{{f.Category}}</td>
        <td>{{f.MktgCode}}</td>
    </tr>
</tbody>

render ~ 7 seconds

Added bind once attribute (available since angular 1.3) (although it is not applied to ng-repeat directive

<tbody>
        <tr ng-repeat="f in vm.rowDetails">
            <td class="checkbox-column"><input type="checkbox" ng-model="f.selected" /></td>
            <td class="w100">{{::f.Code}}</td>
            <td class="w50">{{::f.Class}}</td>
            <td>{{::f.WebName}}</td>
            <td>{{::f.Category}}</td>
            <td>{{::f.MktgCode}}</td>
        </tr>
    </tbody>

no discernible improvement. I suppose this is somewhat expected since this optimizes subsequent watch cycles.

Started experimenting with doing my own row string concatenation.

<tbody>
    <tr pm-table-row f="f" ng-repeat="f in vm.rowDetails track by $index"></tr>
</tbody>

directive:

angular.module('app').directive('pmTableRow', ['$interpolate', function ($interpolate) {
    var template2 = '' +
        '<td class="checkbox-column"><input type="checkbox" ng-model="f.selected" /></td> ' +
                        '<td class="w100">Code</td>' +
                        '<td class="w50">Class</td>' +
                        '<td>WebName</td>' +
                        '<td>Category</td>' +
                        '<td>MktgCode</td>';
    return {
        restrict: 'A',
        replace: false,
        scope: { f: '=' },
        link: function ($scope, $element, $attr) {
            var fields = ['Code', 'Class', 'WebName', 'Category', 'MktgCode'];
            var t = template2;
            var f = $scope.f;
            for (var k in fields)
                t = t.replace(fields[k], f[fields[k]]);

            $element.html(t);
        }
    }}]);

This seemed like an improvement but not a huge one.. render went down to ~4.7 seconds

finally i tried removing ng-repeat completely, and generate the tBody string in my directive

<tbody pm-table-body items="vm.rowDetails">

directive:

angular.module('app').directive('pmTableBody', ['$interpolate', function ($interpolate) {
    var template2 = '' +
        '<tr><td class="checkbox-column"><input type="checkbox" ng-model="f.selected" /></td> ' +
                        '<td class="w100">Code</td>' +
                        '<td class="w50">Class</td>' +
                        '<td>WebName</td>' +
                        '<td>Category</td>' +
                        '<td>MktgCode</td></tr>';
    return {
        restrict: 'A',
        replace: false,
        scope: { items: '=' },
        link: function ($scope, $element, $attr) {
            var fields = ['Code', 'Class', 'WebName', 'Category', 'MktgCode'];
            var lines = [];

            $scope.$watch('items', function () {
                var items = $scope.items;
                var t1 = new Date();
                var t = template2;
                for (var i = 0; i < items.length; i++) {
                    var f = items[i];
                    for (var k in fields) {
                        t = t.replace(fields[k], f[fields[k]]);
                        lines.push(t);
                    }
                }
                console.log('pmTableBody html concatenation done in: ', (new Date() - t1)/1000); // done in 0.02 seconds 
                $element.html(lines.join(''));
            });
        }
    }}]);

for some reason this increased the rendering time to 28 seconds.. so I am likely missing something obvious here.

I would like to bring render time to < 1s.

update i've removed track by $index by creating a unique id field on my duplicated objects. so far no discernible change in performance but i'll keep trying.

update i've added a watch counter based on this post. and here are the results: with 4400 rows and ng-repeat approach, there are 26,102 watchers with the tr pm-table-row approach, there are 4457 watchers, and finally with pm-table-body approach, there are 57 watchers! interestingly this is by far the slowest performing approach.

update interestingly after profiling further the pm-table-body approach seemed to use a lot of angular-animate features. enter image description here

after disabling animation, 10 seconds is taken by (program) enter image description here

so the browser is still unresponsive for a very long time, still troubleshooting what's happening there.

Answer:1

If you'd like to increase your rendering perfomance you should use limitTo filter and implement pagination on your page, another approach is to use this ngInfiniteScroll directive to view whole table on one page.

And if you're wondering about some kind of search in this table, another filter ng-repeat="item in items | filter:searchString" will be applied to the whole dataset, not only to your limited view, but you need to take in mind that filter filter should follow first before limitTo to works so.

Answer:2

Try putting the bind once attribute in the ng-repeat directive:

<tr ng-repeat="f in ::vm.rowDetails track by $index">
Answer:3

A couple of things that may help:

I know you said that you are track by $index because you duplicated your entries for the test, but if f has a unique property, like ID, it will speed up perfomance to track by f.id.

If you're using ngAnimate, disabling it can result in faster render times.

http://ui-grid.info/ does some things with only rendering items that are in the viewport, so you still have them all in the same list, but only things you can currently see are rendered. There are some drawbacks to uigrid.

Lastly, in general this is just a downside of angular, ng-repeating across large sets is not performant. You may need to just render the table wholly outside of a ng-repeat (especially if this is bind once, you don't need a lot of the dynamic power ng-repeating gives you), or either implement pagination or infinite scrolling.

Answer:4

I'm stuck on a making a simple loading function for an AJAX request. AJAX is still pretty new to me, so I'm sure I'm just doing something dumb! ajaxStart fires and starts loading the spinner.gif, ...

I'm stuck on a making a simple loading function for an AJAX request. AJAX is still pretty new to me, so I'm sure I'm just doing something dumb! ajaxStart fires and starts loading the spinner.gif, ...

I'm trying to attach onclick events to elements created dynamically, but only the last element captures the event, ... why? Here the code: <!DOCTYPE HTML> <html> <head><title&...

I'm trying to attach onclick events to elements created dynamically, but only the last element captures the event, ... why? Here the code: <!DOCTYPE HTML> <html> <head><title&...

  1. dynamically creating elements in javascript
  2. react dynamically generated elements
  3. jquery dynamically generated elements events
  4. jquery loop through dynamically generated elements
  5. jquery select dynamically generated elements

I want to create a new ObjectId in the mongo shell but for a Date in the past in order to simulate the creation ob this document in the past. That would be the opposite of the getTimestamp() function ...

I want to create a new ObjectId in the mongo shell but for a Date in the past in order to simulate the creation ob this document in the past. That would be the opposite of the getTimestamp() function ...

  1. create objectid from string
  2. create objectid in mongoose
  3. create objectid in mongodb

I'm having a little difficulty programming the DateTextBox widget to operate with a month and year only input, and to put a mm/yy format in the DateTextBox. Can anyone help me tidy this up? <...

I'm having a little difficulty programming the DateTextBox widget to operate with a month and year only input, and to put a mm/yy format in the DateTextBox. Can anyone help me tidy this up? <...