Angular watchers: from 12500 to 0

journey to optimization

Recently i had to show some data in Angular app. Not big deal: couple of tabs with lists, divs and links. And while this data was static and i already knew about one-time binding, i started to use double colons from very beginning. Like this:

{{::item.name}}

But… performance was… eeehh… not good. Took couple of seconds until page was fully rendered.

Suspect #1

After some google’ing, i decided to investigate watchers. After all, all my data is static and there should be no (or almost no) watchers. Here into rescue comes Chrome’s extension “Angular performance” (https://github.com/Linkurious/angular-performance) .

Run the app, F12, switch to tab Angular and …

4608 watchers!!!???… What???… Hmm…

Ok. Definitely, there are some directives - like ngIf, ngClass — which are watching for changes which will never happen. I need to get rid of them!

So… Let’s remove all one-time bindings and see how much watchers will be there if app was written in default way. Just for curiosity.

“Angular performance” report

12441 watcher. Not bad… Page rendering of course became slower. How much? Don’t know. I don’t like it. Isn’t that enough? :) If you know how to calculate time, let me know.

One-time expression

I think this feature was introduced in version 1.3. And helped in situations like i have. So, let’s add one-time binding back. And we get 4608 watchers again. Making simple one-time expressions like this

{{::item.name}}

reduced watchers drastically. Of course, it depends on the app and data, but in my case, it’s around 2/3.

Thing is we can apply same double colon for all other expressions:

<my-directive item="::item"></my-directive>

Watchers left: 3884

<li ng-repeat="item in ::items">...</li>

Watchers left: 2183

<div ng-if="::isVisible"></div> 
// or
<div ng-include="::template"></div>
// or similar

Watchers left: 1177

// ui-router
ui-sref="item.view(::{id: item.id})

Watchers left: 509

Subtract couple of hundred watchers used in navigation, notification, login form, etc. (needs optimization of course) and we’ll have ~300 watchers left. But there should be none.

Another Chrome extension, called ng-inspect-watchers (https://github.com/ryanoglesby08/ng-inspect-watchers), can help here. When enabled, it shows how much watchers are in specific part of the page; just hover some div or list, and plugin shows number of these little bastards.

“ng-inspect-watchers” in it’s duty

Ok. I found them. And there should be none, because one-time binding is applied. “Aha, found the bug in Angular!” — i thought. Unfortunately… Angular documentation says:

One-time binding expressions will retain the value of the expression at the end of the digest cycle as long as that value is not undefined. If the value of the expression is set within the digest loop and later, within the same digest loop, it is set to undefined, then the expression is not fulfilled and will remain watched.

RTFM!! :)

To solve this i added fallback:

{{::item.qr || 'none'}}

And wuala.. 0 watchers. I still don’t like how page is rendered and there are some other optimization ahead. But at least there are no useless things going on.

Instead of conclusion

Always use one-time expression if you know that printed data won’t change. It will definitely save digest time. I may be wrong, but Angular v2 will have one-time (one-way) binding by default and you will need to set two-way binding yourself. For me it sounds reasonable.

Thanks for reading. Good luck.

Show your support

Clapping shows how much you appreciated Mindaugas Murauskas’s story.