codelord.net

Code, Angular, iOS and more by Aviv Ben-Yosef

Techniques for Improving ng-repeat Performance

| Comments

ng-repeat is notorious for often being the biggest performance bottleneck in Angular apps. It is common practice, when performance tuning lists, to use track by or one-time bindings. Surprisingly, trying to combine both of these practices usually has unexpected results. In this post, we’ll go over the different scenarios ng-repeat is usually used for, and the possible improvements for speeding it up ng-repeat.

Note that before diving into any optimizations, I highly suggest measuring and tracking down the problematic parts of your app.

Refreshing the whole list

Scenario

The list you’re rendering with ng-repeat isn’t edited/changed on a per-line basis, but simply reloaded, e.g. with a “Refresh” button that fetches the model again from the server and replaces the currently displayed list.

The act of re-rendering the whole list feels laggy. Usually this means there’s some jank, or the browser seems to freeze for a bit before actually rendering.

Improvement: track by

Use track by as detailed here. In the above scenario, when the list is changed Angular will destroy all DOM elements in the ng-repeat and create new ones, even if most of the list elements remained identical to their previous versions. This, especially when the list is big, is an expensive operation, which causes the lag.

track by lets Angular know how it is allowed to reuse DOM elements in ng-repeat even if the actual object instances have been changed. Saving DOM operations can significantly reduce lag.

A completely static list

Scenario

Your component displays a list of items that’s never changed as long as the component is alive. It might get changed when the user goes back and forth between routes, but once the data has been rendered and until the user moves on to a different place in your app, the data remains the same.

This is common in analytics apps that display results of queries and don’t actually manipulate the data afterwards.

While the list is on the screen the app might feel sluggish when interacting with it, e.g. clicking on buttons, opening dropdown, etc. This is usually caused by the ng-repeat elements introducing a lot of watchers to the app, which Angular then has to keep track of in every digest cycle.

I’ve seen my fare share of apps with simple-looking tables that made an app grind to a halt because under the hood they had thousands of watchers, heavy use of filters, etc.

Improvement: one-time bindings

Just for the case where the data should be rendered for a single time we were given the one-time binding syntax.

See the full explanation here, but basically by sprinkling some :: inside the ng-repeat elements’ bindings, a static list can have the number of watchers reduces to even nothing.

This means that once the rendering has finished the table has no performance impact on the page itself.

Editable content

Scenario

Your app displays a list or a table that can be changed by the user. Maybe some fields can be edited. Or perhaps there’s a table cell with a dynamic widget.

The list is big enough to have the same performance problems discussed in the previous part, but, since it needs to pick up on changes in the data, simply using one-time bindings breaks its usability. The list stops displaying changes, since all our watches are gone.

Improvement: immutable objects and one-time bindings

When you mutate one of the objects inside a list, Angular doesn’t recreate the DOM element for it. And since the DOM doesn’t get recreated, any one-time bindings will never be updated.

This is a great opportunity to start moving into the more modern one-way data flow and use immutable objects.

Instead of mutating the objects, e.g.:

1
2
3
function markTodoAsDone(todo) {
  todo.done = true;
}

… treat your models as immutable. When a change is needed, create a clone of the data and replace the model object inside the list:

1
2
3
4
5
function markTodoAsDone(todo) {
  var newTodo = angular.copy(todo);
  newTodo.done = true;
  todosListModel.replace(todo, newTodo);
}

Why does this work? Consider this template:

1
2
3
4
5
<div ng-repeat="todo in $ctrl.todosListModel.get()">
  <div ng-bind="::todo.title"></div>
  <div ng-if="::todo.done">Done!</div>
  <button ng-click="$ctrl.markTodoAsDone(todo)">Done</button>
</div>

Since all the bindings inside the ng-repeat template are one-time bindings, the only watcher here is the $watchCollection used by ng-repeat to keep track of $ctrl.todosListModel.get().

Had we simply mutated the todo object, the $watchCollection would not get triggered and nothing would get rendered. But since we’re putting a new object inside the list, $watchCollection will trigger a removal of the old todo, and an addition for the new one. The addition will create a new DOM element for our todo, and so it will get rendered according to its latest state.

Summary

ng-repeat can be tamed, at least partially, if you take care to use it according to your specific use case.

I hope this helps you speed your app a bit!

“Maintaining AngularJS feels like Cobol 🤷…”

You want to do AngularJS the right way.
Yet every blog post you see makes it look like your codebase is obsolete. Components? Lifecycle hooks? Controllers are dead?

It would be great to work on a modern codebase again, but who has weeks for a rewrite?
Well, you can get your app back in shape, without pushing back all your deadlines! Imagine, upgrading smoothly along your regular tasks, no longer deep in legacy.

Subscribe and get my free email course with steps for upgrading your AngularJS app to the latest 1.6 safely and without a rewrite.

Get the modernization email course!

Comments