AngularJS: Performance basics

Angular has a built-in change propagation mechanism, based on digest loop.

Note: if you are not familiar with the terms "digest loop" and "digest cycle", please spent some time studying the available info. Understanding the digest cycle is very important to be able to tweak angular apps.

This mechanism is very similar to javascript event system:

Javascript system Angular system
event change
event type watch value
event bubbling change propagation
event listener watch callback

Terms

Change - a single event in Anguar world, which must trigger digest cycle. This can be a browser event or a model field change. This is a thing which makes you call scope.$apply in your custom directives.

Watched value - the result of watch expression evaluation. This is the value which will be checked in the current scope on each digest cycle.

Watch callback - a callback which will be triggered by digest loop when the watched value in the current scope have changed (see $watch docs for details)

Watch cascade - the process of change propagation through the scope hierarchy, resulting in watch values comparison and watch callbacks invocation.

Full watch cascade - the watch cascade starting from the $rootScope and going through all the child scopes.

Change triggering

In angular world you have to trigger the change event by starting the digest cycle either by calling scope.$apply or scope.$digest.

You need to do that to let other parts of angular world to know that something has changed and those parts need to react to the change.

If you are using the built-in directives and services or putting the data in the $scope directly, Angular will handle changes for you.
If you are creating custom directives and services, you have to handle the changes manually.

Scopes and Changes

The scope hierarchy is a central bus for change propagation.

Each scope in the hierarchy can react to any change made in the whole hierarchy.

Each $scope contains a set of watched values and watch callbacks, which are registered using the $scope.$watch call.

Watch values are like event types, and watch callbacks are event listeners in ordinary javascript event system.

Note: Every {{ expression }} in the template registers a watched value and watch callbacks, which updates the DOM node (this is how a bidirectional binding is made).

A watch callback fires when:
1.digest cycle is triggered in the current scope (scope.$digest or scope.$apply is called)
2. watched value is different from the previous digest cycle

Change Propagation

Angular propagates the change down through the scope hierarchy. This is what I call a watch cascade.

So, when you update some scope value in the controller, all the underlying scopes will be affected by change propagation.

It means, that each single change causes all scopes to run digest cycles and check the watched values.

Full Watch Cascade

Full watch cascade is the default behaviour for Angular.

Angular assumes that if you have a change in some part of your system, this change can result in state update in another part of the system.
That's why all this stuff will trigger full watch cascade:

  1. All event listener directives (ng-click,ng-mousemove, etc).
  2. $timeout, $interval, $http and $location services.
  3. Form elements directives interaction (form, select, input, etc)

Angular's scope events ($on, $broadcast, $emit) do not call $scope.$apply!

You should really understand those implications:
Each scope.$apply call runs a digest cycle for the whole scope hierarchy in your project.

Full Watch Cascade starts from the $rootScope and propagates to all the child scopes using DFS algorithm.

There is no difference between calling $rootScope.$apply and $scope.$apply. Any of those calls will run the full watch cascade starting from the $rootScope.

If you want to limit the cascade, you need to control the change propagation manually. There are a couple of approaches:

  1. Use scope.$digest instead of scope.$apply in your directives
  2. Call $timeout service with 3rd optional argument set to false: $timeout(fn, delay, apply)

Note: This techniques can cause bugs, as you may have a dependent state in sibling scope hierarchy. Use it with caution and confidence!

OMG! WAT? Angular is Sloooo...

Angular is not inherently slow.
But it can be made slow relatively easy.

If you ever wonder "why the hack digest cycle instead of observers?! It's soooooo sloooooo!", then start studying the arguments from Misko's answer.
It is a great idea to study all those arguments yourself to make you head around angular intrinsics.

If you are concerned about your app's performance, then you must have a well-defined metrics and a way to measure everything. Until then, you will stuck with premature optimisations, which is not a good idea.

Takeaways

  1. Angular performance depends highly on digest cycle and scope hierarchy.
  2. You need to know angular intrinsics to tune the performance.
  3. There is a set of performance tuning tips you need to know to write efficient code using AngularJS.