Blog

Improving Angular Dirty Checking Performance

We build a lot of search applications, including our search relevancy workbench, Quepid or the US Patent and Trademarks Offices Global Patent Navigator Search. A big part of making clients happy is nailing the frontend – making search actually usable! In doing this we love Angular, but weve certainly had to guard against one performance anti-pattern – excessive dirty checking.

Angular provides convenient two-way data binding. Its an extremely convenient way of building rich templates that somehow magically seem to know to update themselves in the DOM. I can easily write some declarative HTML that expresses the content Im trying to put on the screen:

{{companyName}} Employees

  • {{employee.fullname()}}

Instead of manually issuing a command to update a template, something magical in Angular knows somehow that the variables bound to the current $scope (in this case and ) have changed – and therefore the associated snippets of HTML need to be updated.

The Dirty (Checking) Secret of Two-Way Binding…

How does Angular magically know how to update the content? It uses a technique called dirty checking. Angular tracks the previous values for these various expressions. If it notices a change, the corresponding Angular directive is given a chance to reflect those changes in the DOM. So if employee.fullname() changes, the interpolation directive (the fancy name for the double curly-braces syntactic sugar) has a chance to reflect this change in the DOM.

All of this checking happens by methods on $scope. A $scopes digest cycle runs the dirty check, evaluating expressions on $scope, checking them against previous values, and overwriting the previous values with the just calculated ones. This digest cycle is triggered by angular directives. For example, ng-click is simply a directive that binds to the corresponding elements click event. On click, the directive evaluates an expression, perhaps calling a function or perhaps doing a simple assignment. For example, maybe the click caused the variable “login” to be set to “true”. Once whatever data has been modified, the direct can then trigger the digest cycle (through $scope.$apply).

On the receiving end of the two-way binding are bits of angular code (directives or otherwise) waiting to respond to changes. This is what you see in your angularized HTML with expressions such as ng-repeat="employee in employees". Ultimately these directives register with $scope.$watch (these are the expressions being dirty checked). The job of the digest cycle is to identify all the expressions registered for watching (via $scope.watch), see if theres a change, and execute the registered callback. We might have $scope.$watch on the login flag from the previous paragraph. Our callback might update the DOM or transition to a new page.

This is both Angulars secret sauce and Achilles heel. With most applications, its likely not a problem. With Quepid, however, we had a problem of repeated elements that could be clicked into. Once you were inside each element, you could interact with dozens of little widgets.

From an implementation point-of-view, each of these widgets was hidden until they were clicked, something like:

    
...

Unfortunately, “hidden” does not stop the dozens of little Angular widgets from registering with their scopes and being dirty-checked during the digest cycle. After enough of these repeated elements, the application would drag significantly. Using a profiler, we could clearly see the issue was in $scope.digest – Angulars digest cycle.

So what can be done to deal with this problem? Let me walk you through some of the strategies that have worked for us.

1. Use ng-if, not ng-show

One of the best features to come out of Angular 1.2 is ng-if. ng-show works by applying “display: none” on or off of the element – thus hiding it but keeping it in the DOM. Unfortunately, by keeping it in the DOM the directives within are still watching for change and doing work. Ng-if on the other hand delays compiling the elements into the DOM until the condition is true. Effectively this is a way to lazy-load angular capabilities into the DOM, and thus avoid having hundreds of pointless dirty checks slowing down the application.

The downside is, ng-if involves a small performance hit as it must fully compile the Angular template on-demand. This might be able to be mitigated with a combination of Ng-show and Ng-if. What if Ng-if was tied to a hover state, but paired with an Ng-click that was tied more closely to a click state?

Perhaps future versions of Angular could combine some lazy-loading functionality from ng-if with the new snazzy shadow-DOM that somehow could do part of the work without registering all the little expressions that need to watch for changes on the DOM.

2. Make sure what is being checked is cheap

With frequent dirty checking, its inadvisable to place calls to complex functions into Angular expressions. Remember, this stuff will get called A LOT! An ng-click somewhere could trigger a lot of dirty checking. Thus Any lengthy calculation should be avoided. For example, sorting a big list and returning the top element would probably be a big no-no. Instead, have a way to check to see if the list has changed, then take some kind of action (see #3).

3. For manual watches, use a single watch a version or a hash of the thing (not lots of little watches)

Instead of

$scope.$watch("employee.firstName", function() {...})$scope.$watch("employee.lastName", function() {...})

Combine these into a single watch over a hash or other value that will change every time a state changes. An obvious solution is to give employee a hashing function that always returns a unique value:

Employee.Hash = function() {    return this.firstName + this.lastName + this.Age ...;}

Then elsewhere:

$scope.$watch("employee.hash", function() {});

Hash functions can collide. They might also be expensive. So lately, Ive been using a version number that increments on every change for these problems:

this.setFirstName = function(newFirstName) {    this.firstName = newFirstName    this.version++;}

Its an implementation burden, but lets me write code that just checks if a single integer has changed:

4. Dont use $scope.$watch for events

When you start out with Angular, its tempting to pass events up and down the $scope hierarchy by setting status at one level and performing a $watch on it elsewhere. For example in a root scope we might set:

$scope.userLogedIn = true

And in child scopes, we might want to respond:

$scope.$watch("userLoggedIn", function() {});

Alternatively, dont clutter up the digest cycle with event-like expressions. Use Angulars explicit event features like emit and broadcast.

5. Avoid Dirty Checking Entirely by working straight with the DOM

One of my favorite things about Angular is that I reserve the right to not use Angular if need be. Even in Angular world, I can simply use a directives link function to interact directly with the DOM as needed. For example, I could chose to simply insert a handlebars template into a div instead of worrying about getting angular-ese right:

link: function(scope, element) {    button = element.find("button");    button.onClick = function() {        // find our list!        employeeList = element.find("employeeList");        employeeList.innerHtml = "
  • No Employees, Only Ninjas!
  • " };};

    Anyway, those techniques have helped us tremendously in our work. I hope youll also find them useful. Please comment if you have anything youd add to this list! And of course if you have problems with your search UI or would simply like a better search application, contact us to see if we can help!