codelord.net

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

Angular Performance: Updating bind-once elements

| Comments

Performance can be tricky with Angular. It’s quite easy to have a page that’s slow or unresponsive because you got too many watchers going on.

Angular 1.3 shipped with the new bind-once syntax, but I hardly see people use it. Even if you add it, you hardly never want things to never change again.

Usually, you have a bunch of stuff that change together. Plainly using bind-once would remove all the watches and make your page responsive and your digest cycles fast. But, how would you go about updating it?

It’s a bit tricky, but doable. In this post we’ll show an example of performing such an optimization.

Our use case: search results

Let’s say you have a page in your app that let’s you search something. You get from the server a response with 100 items that you them display:

1
2
3
<div ng-repeat="item in $ctrl.results">
  <div>{{ item.name }} - {{ item.description }}</div>
</div>

If we display 100 results at once we already have 200 watches: 2 bindings (name and description) per item displayed.

This can easily get out of hand.

But look at your results. They’re not going to change individually. The will only change as a whole, when, for example, the user goes to the next page or types a different search query.

Let’s get rid of them watches!

Binding once

You might first start with something trivial like this:

1
2
3
<div ng-repeat="item in $ctrl.results">
  <div>{{ ::item.name }} - {{ ::item.description }}</div>
</div>

This simple change (adding :: in two places) rids us of those 200 watches. Yay!

But, what would happen if you now update the description of items inside $ctrl.results, e.g. because the user marked them as read?

Nothing.

It’s a bit crappy that there’s no easy way to tell Angular to refresh these bind-once expressions.

But don’t give up yet.

Note: In this example, if you add/remove elements from $ctrl.results they will be added/removed to the ng-repeat. If you never change the inline elements you wouldn’t need anything more than this. This is a simple example, but pages with performance problems usually aren’t simple :)

Forcing an update of bound-once content

This requires a bit of coding. Basically, we want to tell Angular to throw away those frozen elements and recompile stuff to use our new content.

Our end result would look like this:

1
2
3
4
5
<refresher condition="$ctrl.lastUpdate">
  <div ng-repeat="item in $ctrl.results">
    <div>{{ ::item.name }} - {{ ::item.description }}</div>
  </div>
</refresher>

We wrap our content with a specially crafted directive, refresher. This directive listens for a given property. Once that property changes its value, refresher recompiles the content inside it.

This means that we effectively have a single watch active, and once that watch’s value changes everything updates.

In our case, we can have lastUpdate be whatever you want as long as it is updated when your content changes. I usually just use a timestamp that I update whenever the content changes.

Here’s how refresher looks:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
app.directive('refresher', function() {
  return {
    transclude: true,
    controller: function($scope, $transclude,
                         $attrs, $element) {
      var childScope;

      $scope.$watch($attrs.condition, function(value) {
        $element.empty();
        if (childScope) {
          childScope.$destroy();
          childScope = null;
        }

        $transclude(function(clone, newScope) {
          childScope = newScope;
          $element.append(clone);
        });
      });
    }
  };
});

There’s not a lot of code going on around here.

refresher is a basic directive (why not a component? because components have to have isolated scopes, which I don’t want in this case).

This directive basically watches for changes in the value of whatever you pass to its condition attribute.

Whenever that value changes, refresher throws out whatever it previously contained and recompiles things, so they’ll appear on screen updated.

This is a basic example of using the $transclude service manually.

That’s it!

“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