Is Angular really that slow?

I read many opinions that AngularJS is a framework for creating slow frontend applications. Authorities on the topic provide numerous arguments about this and I mostly agree with them. This is particularly true when compared to other frameworks like react.js.

The thing is, there are hundreds of developers complaining about poor performance of their apps while they do nothing or almost nothing to actually deal with it. Trust me, bindonce is not enough if you spam $scope.$apply() dozens of times per second. Sure, Angular is not a performance eagle, but when an application hits number of over ten thousand active watchers simultaneously...to me, those examples indicate bad design, not a slow framework.

Angular provides us with tremendous power, comfort and the possibility to quickly create complicated applications, but

With great power comes great responsibility

In this post I’d like to share a few non-trivial, less-known hints and techniques to improve Angular’s performance. Some are standard, some aren’t but should be, and others aren’t and shouldn’t be, but sometimes - you hit the ceiling and there is no longer a choice.

Performance measurement

First, I’d like to tell you how I measure performance of Angular apps:

  • Chrome DevTools - great built-in tool in Google Chrome, for general performance measurement of web pages/apps. I use it to track when and what part of the app causes performance issues.

  • ng-stats - tool for counting the number of watchers and the time taken by the Angular digest cycle. I use this one because of it’s simplicity, but similar tools also exist if you have more sophisticated needs.

  • performance.now and console.time - Javascript functions for measuring time. I use them to measure execution time of particular parts of code.

That’s it; enough for me. If you have some other, better tools/techniques, please share them in the comments. I’d be happy to update the list.

Ng-Show vs Ng-If: additional round

I’ve seen many discussions about which directive should be used to show and hide elements depending on a particular condition. Everybody agrees that it depends on the case:

  • use ng-show if you often toggle element visibility, ng-if otherwise

  • use ng-if over ng-show if you want to remove elements from the DOM and watchers from scopes

I’d like to add one rare case:

  • use ng-show if your element is very big, heavy and renders too long

This is a rather rare case, but is possible and often has a very bad impact on the UX. In addition, if an element is big and renders long, it possibly has many watchers slowing down the Angular digest cycle. We’d like to somehow disable those watchers. I described one way of doing this in my previous post.

This also applies to ng-repeat, which often re-renders its elements. If you have a list of heavy elements and often manipulate it, it’s a good idea to render them once using ng-repeat and then just show/hide using ng-show (while disabling watchers if necessary).

Watch your watchers

This topic is as old as Angular itself.

The number of watchers existing simultaneously is a critical concern when considering Angular performance. More watchers means a longer digest cycle and a long digest cycle can cause performance issues, especially if triggered often.

The best solution is to avoid creating very complicated views with lots of watchers. If you cannot do that, there are other ways that can help you.

An example is simple techniques like bindonce and built-in one-time binding (since Angular 1.3) - use them for rendering static content.

Although they are simple and helpful, they end up being useless when considering dynamic content. In that case, there is no golden rule. You can use clever solutions like:

  • disabling watchers of not visible/inactive elements

  • creating full behavior of element on hover

But the most important thing is to think twice and remember about primary rules:

  • avoid complicated watcher’s evaluation

  • watcher’s callbacks should not change its own or other watched values to avoid re-triggering the digest cycle

All this may sound complicated, but it quickly becomes habit.

Digest and avoid indigestion

There is no way to think about the higher level of Angular performance without understanding a few things:

  • how digest cycle works in general

  • difference between $scope.$apply() and $scope.$digest()

  • list of Angular core features triggering digest cycle

In my opinion, the difference between $scope.$apply() and $scope.$digest() is critical when building big and sophisticated directives. Many people abuse $apply(), if not directly, then in $timeout, $interval and other features that trigger the whole app digest cycle. It’s often necessary and sometimes it’s justified. But sometimes it isn’t. Let’s talk about the latter.

Directives are usually responsible for and only change small parts of the app. So why do we recalculate the whole app to apply only small state change? If a directive changes only its internal state or the DOM, then $scope.$digest() is what we need. This way, the digest cycle is limited to a directive's scope and its children, which is enough to apply those changes. It’s much more efficient, because there are fewer things to recalculate. I beg you, for the sake of your app - look at your directives again and reconsider the way you apply the changes outside of the Angular magic.

This also applies to core Angular features like $timeout and $interval which I mentioned earlier. They trigger $scope.$apply() themselves. Sometimes I replace them with:

setTimeout(function() {
    // your code
    $scope.$digest()
}, 100)

setInterval(function() {
    // your code
    $scope.$digest()
}, 100)

Some people will call this hacky, but others have no choice. This technique usually gives me the best performance boost.

Summary

The techniques discussed above have allowed me to really improve the most critical parts of my applications. The list is not long - I tried not to duplicate content that I read in other articles and tutorials.

Angular is a big, powerful framework, but has its downsides and limits. Design that takes them into account should not even let you think about most of what I mentioned here. I hope this advice will help people to better understand their framework and its performance. If you use all of it and still suffer...well, there is always lightning react.js.