Optimizing ng-repeat in AngularJS

The ng-repeat directive is the workhorse for any AngularJS application. It is almost always one of the first things you encounter when you get started. You hardly think about optimizing ng-repeat in AngularJS when you get started, because that is rarely the first concern you start with.

Using ng-repeat in AngularJS

Let’s quickly recap how we can use ng-repeats (along with optional filters):

<div ng-repeat="stock in ctrl.stocks | filter:someCondition">
<!-- Repeat Template Content here -->

We can also use ng-repeats to iterate over objects (In case you hadn’t known this):

<div ng-repeat="(key, stock) in ctrl.stocksMap | filter:someOtherCondition">
<!-- Repeat Template Content here -->

where stocksMap is an object map, where the key is the id of the stock, and the value is the individual stock object. One common misunderstanding now is how AngularJS creates and displays the UI, based on this ng-repeat. This is how AngularJS works under the covers (in a nutshell) when you use ng-repeat:

  1. It iterates over each item in the array (or each key, value in the object)
  2. It runs each item via any filters that are present in our expression, to check if it should be displayed or not
  3. It calculates a hash value by which it identifies the object (which is by reference by default)
  4. It checks if it has already created a DOM element for the hash value previously
    1. If so, it reuses it
    2. If not, it creates a DOM element based on the ng-repeat template
  5. All the DOM manipulations are taken and inserted in an optimal manner into the actual DOM
  6. A watch is added on the array, which triggers step 1 again if the array undergoes any change

With that covered, now let us see how you might unknowingly impact performance or how we could improve the performance:

Optimizing ng-repeat in AngularJS

There are two quick optimizations we can do, and one not so quick optimization to increase the performance of our application when using ng-repeat.

Use the track by syntax

With every ng-repeat expression in AngularJS, there is an optional track by syntax we can use, which is used as a hint to AngularJS to tell it how to recognize unique items. For every item AngularJS displays in an ng-repeat, by default, it calculates a hash value to uniquely identify the object. This is how AngularJS decides whether it has seen an object before, or if it has to create a new DOM element for the object.

Now, if we just fetch an array once, and always manipulate it, then we don’t have to do any additional work. But if we fetch our array multiple times, or the reference to the objects displayed in the ng-repeat change multiple times over the course of our application, then it is a good idea to tell AngularJS how to identify unique items. For example, if we have a unique id field on each object in the array, we could use the track by syntax as follows:

<div ng-repeat="stock in ctrl.stocks track by stock.id"></div>

Optionally, with a filter, it could be used as

<div ng-repeat="stock in ctrl.stocks | filter:someFilter track by stock.id"></div>

This tells AngularJS that each stock is uniquely identified by the stock.id value, and to reuse DOM elements as long as the id field does not change. This is a quick and simple step to optimizing ng-repeat in AngularJS, to ensure DOM manipulations in your application are reduced.

Filter in your controller

Filters (and especially the badly named but super useful filter filter) are a great way of quickly formatting or filter your data, without affecting the underlying model object. They are great for converting your numbers into currency, or adding decimals, or for filtering arrays of data under certain conditions.

But filters, when used directly in your HTML (as seen the examples above), have an underlying implication:

An AngularJS filter in your HTML will execute (multiple times!) for every digest cycle

This means that your filtering function, or your filter will execute multiple times, even if the underlying data or conditions have not changed. And this is because of how AngularJS does its dirty checking (which is an article for another time).

Now, instead of this executing multiple times for no reason whatsoever, if you in your application have control over when filtering is actually done (say on the click of a button), then you can optimize by triggering your filter in your controller instead of having it directly in your HTML.

Any AngularJS filter can be injected into your controller, service or directive by adding the keyword Filter at the end of the name of the filter, and then specifying that in the dependency injection syntax. The JSFiddle below shows an example of performing the filter both in your HTML as well as in your controller.

Functionally, both are the same, but underneath, the filter in the controller approach is just a bit faster and more performant.

Use bind-once

AngularJS 1.3 introduces a concept of lazy single binding, which is used to just get data out to the UI once at the beginning. After that, AngularJS stops watching the variable and updating it in the UI. It is useful for data that you don’t expect to change once displayed to the user. This was inspired by bindonce, a third party module which introduced this concept.

AngularJS version 1.3 uses a lazy, bind once and forget syntax, which can be done for any AngularJS expression by prefixing :: before the expression. That is

<span ng-bind="::stock.name"></span>

Would print the name once it has been loaded, and stop watching it after that. Similarly, for ng-repeats, it could be used as

<div ng-repeat="stock in ::ctrl.stocks">{{::stock.name}}</div>

Of course, this is only possible in the latest version of AngularJS. For older versions, use the third party bindonce module we mentioned above, which offers the same functionality through the use of a new AngularJS directive.

About Fundoo

At Fundoo Solutions, we specialize in AngularJS, NodeJS and Mobile. Tips like these are just scratching the surface of how we can help you out. Whether it is full-day, hands on workshops or architecture or development consulting (providing advise / guidance or actual development), or just more tips and suggestions like these, reach out to us over email, or follow us on twitter.

ShyamOptimizing ng-repeat in AngularJS
  • Silvan Hofer

    Would be very nice if this hash key really would be unique…

    I spent quite some hours figuring out why my ng-repeat doesn’t work anymore.
    First I thought my data is corrupted somehow that not every expression can be evaluated anymore.

    Now that I managed to catch the angular exception, got an error message and know, what $$hashKey actually is, I see the problem:
    ng-repeat throws an exception because of duplicate keys which Angular created by itself (“$$hashKey”:”object:31″)!!

    But for me it is not clear, why it does this (add object to array, save array with JSON.stringify to localStorage, add another, overwrite localstorage with json.stringify again. close app, open again and try to use the with json.parse restored object array and ng-repeat fails because of duplicate keys).

    • Danielo515

      Angular does this to avoid a dom nodes pointing to several objects. Angular relates each domNode to a single object, if the objects are duplicated angular don’t know to who object they are pointing and throws an error. Instead of allowing angular create their own haskey you can provide one yourself using track by. For example, for arrays that can contain the same string several times you can use track by $index and it will work nicely.

  • Royi

    Hi. I don’t see any reason why putting the source of data as `::xxx` , this means that adding to the source data – won’t be reflected in the UI. Am I missing something ?

    • A.S

      This means that Angular won’t be checking to see there’s any change in the list during every apply cycle (which is run constantly). It might not be too effective for small applications, but for large ones it can make a lot of difference