build with purpose

Repeater Tracking in Angular 1.5

I’ve been switching a lot of my controllers into components thanks to the recent addition in Angular 1.5. The switch has been a great time to dust off some old code and really think about how my bindings are setup. I’ve switched to a lot of single binds in order to improve performance by reducing the number of watchers. However, when I went to throw everything into a repeater element, I started noticing some strangeness. Below is a brief overview of how the problem arose but you can skip to the end if you’re just interested in the outcome.

Eliminating Watchers

Let’s cover some basics. Every time a new databind is defined in a template there’s a watcher created to watch the binding of that data for changes. Digest cycles check for these changes and update the model or view appropriately when changes occur. A basic databind looks a bit like this:

<div>{{value}}</div>

That watcher will exist as long as it thinks the value will continue to change. By default, the watcher will continue to exist because it is permanently tied to a value in its scope.

Now, it’s not very performant to continue to check a value that will only get set once. Every time a digest cycle occurs, this watcher is checked for changes and that takes processing time. Angular provides a great way of handling this by providing single time, one-time databinds as show below:

<div>{{::value}}</div>

Now this databind will still create a watcher. That watcher will continue to exist until the value finally exists in scope. So, until a variable no longer evaluates to undefined, the watcher will continue to check for a non-undefined value. Once a value is found, the watcher will cease to exist and that part of the DOM will become static.

ng-repeat Once

Now these databinds don’t really matter much for the most part. If you have a form or a page that doesn’t have a lot of watchers then it doesn’t really matter too much how those bindings are setup. Where it does start to come into play is on ng-repeat elements. These elements loop through many values and can often require a lot of calculations.

One time databinds are essential for getting great speed out of repeaters and it’s important to know how to use them correctly. Let’s say that I’m going to loop over a bunch of comments and render them on a web page. That might look something like this:

<div ng-repeat="comment in ctrl.comments">
  <h4>{{::comment.name}}</h4>
  <span>{{::comment.message}}</span>
</div>

Now you can see that I’ve gone ahead and added one-time databinds to both the name and message attributes of the comment. This will loop through the comments once and add those values to the DOM if it finds them. Any missing values will leave behind open watchers, but that’s a bit outside of our control from the template perspective. Over all this is pretty performant but there are still some problems hidden under the surface that can cause issues.

Tracking Changes

I’ve seen it commonly suggested that you should add tracking to an ng-repeat for performance reasons. However it took making mistakes for me to best understand how to use tracking correctly. There’s actually quite a bit to it.

I mostly repeat through elements that are objects as in the example above. However, early on when you’re learning how to use Angular ng-repeats will mostly be shown using simple arrays of numbers. We’ll loop through the array and print out all of the values:

<div ng-repeat="n in [41, 42, 43, 44, 45]">
  {{n}}
</div>

Now this works great to show you how to simply use an ng-repeat, however it will error out if you try to do this with a duplicate value in the array. Try it out in a CodePen and let me know how it goes. The reason this errors out is related to how Angular keeps track of elements that it is rendering. The function it uses to determine which DOM element matches the related data will basically hash the value to determine where it belongs. This doesn’t result well when looking at integers because two equivalent values are basically equal hashes.

The basic way to fix this problem is to add tracking to the repeater like so:

<div ng-repeat="n in [42, 42, 43, 43] track by $index">
  {{n}}
</div>

This will assign index values to each item in the array passed into the repeater element. That will allow duplicate values to be passed in because they can be tracked by this index value instead of their given value. Pretty great right?

Now, we don’t need this track by when it comes to looping over objects. Check out this CodePen where you can create many duplicate objects in an array without problem. Just click the button:

I think that the reason this works is because the objects each have a memory location that they can be tracked by. Because they are not simply stored values that are not tied to a memory space they can be tracked individually. That’s at least my theory. If you have a better explanation please leave a comment at the bottom of the page.

So using track by on objects can seem ineffective when looking at it from that approach. However they can still be used for good by allowing Angular to compare a specific identifier for each object rather than calculating a value to track each object at runtime. If our goal is still to improve performance on our Angular app then it’s a good idea to define a value to track for each comment.

<div ng-repeat="comment in ctrl.comments track by comment.id" class="mui-panel">
  <h4>{{::comment.name}}</h4>
  <span>{{::comment.message}}</span>
</div>

The Problem

Awesome. Now we have a nice performant repeater. But what would have happened if we hadn’t had an ID to use for each element and we still wanted to create tracking? It’s tempting to just use the $index again as we did above. However, this will completely and utterly destroy the inner one-time databinds that we’ve been working so hard to implement.

Below is a side-by-side comparison of two repeaters that are looping over the same array. One uses the comment.id as shown above and the other uses $index to track. I’ve changed the message binding to be two-way because it makes it easier to see what’s happening. When we go to add new comments to the top of the list we can see something interesting happening:

What’s happening on the left is that the tracking is re-assigning DOM elements to every element because every element’s $index has changed. But, because of the one-time databinds it’s only able to re-assign the two-time databinds and the rest of the element doesn’t change. Another way to think about it is that a new DOM element has been added at the bottom and every value has shifted down. Because of the one-time databinds, we aren’t able to take our values with us and they stick around.

What’s happening on the right is very different. What I think is happening is that the DOM elements are being moved around based on the comment.id that they are associated with. This allows one-time databinds to be used on repeaters that we want to optimize. This all kind of makes sense but it takes some reasoning about to understand it.

For more information about repeater containers and how they are tracked checkout the Angular Documentation.

Make my day and share this post:

Other posts to peak your interest:

  • Understanding Visual Testing
  • The Redux Saga Black Box
  • What my college degree gave me
  • Technical Leaders Enabling Stronger Teams
  • Finding Your Gateway to Learning Vue
  • Why Write Server Rendered Frontend Apps
  • comments powered by Disqus
    © 2019. All rights reserved.