3

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.

Community
  • 1
  • 1
Sonic Soul
  • 23,855
  • 37
  • 130
  • 196
  • I think my question is, why you wouldn't use a pager or some other service and break the data up after around 100 rows? Who really is ever going to use a web page that renders 4,400 rows of data anyway? – Claies May 13 '15 at 15:53
  • it's good for scanning larger datasets for anomalies. often useful in financial settings or any place with larger datasets. 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 added a note to the top of the question with this. Personally i REALLY hate paging. I think it's a usability nightmare. – Sonic Soul May 13 '15 at 16:01
  • Your last approach takes < 300ms. How do you measure it? Also keep in mind that you should deregister the watcher after the table has been rendered. – a better oliver May 13 '15 at 16:38
  • i wish! it takes 28 seconds.. I am measuring it using chrome profiler, and i can see that n.$apply() is taking up the bulk of the time. but it can also be measured by timing chrome freeze – Sonic Soul May 13 '15 at 16:43
  • Have you tried that one ? https://github.com/allaud/quick-ng-repeat – aorfevre May 13 '15 at 19:13
  • Have you tried this plugin? http://www.angulargrid.com – Juan Marcos Armella May 13 '15 at 19:24
  • @aorfevre thanks, I just tried it but there was no big improvement in my case. ~6 seconds total – Sonic Soul May 13 '15 at 19:24
  • Are you sure your last two experiments work? You never compile the template, so I would expect the `ng-model` on your input not to be wired up. – Dylan Watt May 13 '15 at 20:21
  • it works in the way that it shows the table. i agree that any bound events on those rows will not work.. however even just the html part has above mentioned performance – Sonic Soul May 13 '15 at 20:59
  • In the last example, how many times does your watch end up firing? – Dylan Watt May 13 '15 at 21:09
  • Dylan, what's a good way to measure this? I've been going by chrome profiler ms spent in n.$apply – Sonic Soul May 13 '15 at 21:13
  • The easiest way would be just to see how many time `'pmTableBody html concatenation done in` is written out. On its face, the last example should not take nearly as long as it does. The only thing I can think of is that something is making the render in $watch fire too many times. – Dylan Watt May 13 '15 at 21:19
  • good point. i just checked and that is fired just once – Sonic Soul May 13 '15 at 21:29
  • hey @DylanWatt i've added an update with watcher counts for all scenarios.. very interesting – Sonic Soul May 13 '15 at 23:04
  • Measuring `$apply` doesn't really measure rendering / creation time. Take a look at [this plunker](http://plnkr.co/edit/4JGAj89bZuSEp7toQx5t?p=preview) and you'll see in the console that it takes well below 300ms. I suspect that the problem lies somewhere else, presumably many updates causing many digest cycles. – a better oliver May 14 '15 at 08:19
  • thanks! that helps. as for my time you're probably right, it's something else because there are so few watches in that last approach. definitely not updates though as im not doing much on that page other than show this table and some buttons.. will keep digging – Sonic Soul May 14 '15 at 13:05
  • i think im getting somewhere.. looks like it's angular-animation ! http://i.imgur.com/y06uABz.png – Sonic Soul May 14 '15 at 15:12
  • added an update with latest benchmarks! – Sonic Soul May 14 '15 at 17:23

4 Answers4

0

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.

Anton Savchenko
  • 1,210
  • 1
  • 8
  • 11
  • yes i am aware that rendering less rows will be faster. my question is how to maximize rendering performance speed of a larger dataset. – Sonic Soul May 13 '15 at 16:16
0

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

<tr ng-repeat="f in ::vm.rowDetails track by $index">
Brent Washburne
  • 12,904
  • 4
  • 60
  • 82
  • hmm thanks, but for some reason when I add it there it never renders the contents.. no errors – Sonic Soul May 13 '15 at 16:22
  • removed "track by" completely after making my objects unique, but :: in that spot still preventing anything from rendering – Sonic Soul May 13 '15 at 17:54
  • maybe it works only with scopes, i am using controller as syntax so perhaps it's not supported – Sonic Soul May 13 '15 at 19:26
  • Using `::` in the repeat itself means that `vm.rowDetails` will have to be created with the values. If it's initialized to an empty set (or something like that) it gets rendered as that and then never updates since the watcher is gone. – Tony May 13 '15 at 19:56
0

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.

Dylan Watt
  • 3,357
  • 12
  • 16
  • thanks, yeah i've since came up with a more intelligent way to duplicate it and used a Identity type field to make the unique.. I will update my question since it's no longer a factor, thanks again – Sonic Soul May 13 '15 at 21:01
  • I will evaluate UI grid as they claim good performance on 10k rows. thanks – Sonic Soul May 15 '15 at 12:59
0

I spent a good amount of time flipping back and forth between uigrid, tables, ngrepeat even writing my own version of a virtualized container with ngrepeat. In the end I settled on uigrid and with custom row & cell templates to get it to function and appear exactly how I needed it to. The benefit of uiGrid is it's virtualization, multi column sort & filtering. I am able to scroll, filter & sort 5k+ rows with almost instantaneous response (this includes all my custom row & cell rendering ). Uigrid also has an infinite scroll capability, where it dynamically fetches data from the backend as needed during scrolling, this I'm about to tackle. It should provide a slight performance benefit.

The only draw back I see with uigrid it's learning curve.

Another grid you should take a look at is https://github.com/openfin/fin-hypergrid. This grid seems to outperform uiGrid in some quick test i did, and I would seriously consider switching however I'm too far along with UIgrid to do so.

I hate paging too, a lot of work went into a non-paging solution.

cramroop
  • 248
  • 3
  • 6
  • thanks, I am evaluating UI grid as well. they claim 10k rows so curious how they implemented it! also thanks for they hypergrid! will take a look at it as well – Sonic Soul May 20 '15 at 13:30
  • It's accomplished mainly through virtualization: rendering only the visble rows, while the rest just sits in memory. Check out this plunkr 200k rows http://plnkr.co/edit/8fuIJr?p=preview That is a plain example It should be noted once you start adding your own row and cell templates its not as smooth (depending on how much customization you do.), but still fast. Your row and cell templates are only applied to the visible rows as well. – cramroop May 20 '15 at 13:39