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 |
|
… 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 |
|
Why does this work? Consider this template:
1 2 3 4 5 |
|
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.