Project Development, Coding

Three Ways to Optimize $scope.$apply() and Improve Your AngularJS App

Like many novice AngularJS programmers before me, I thought I struck gold with the $scope.$apply() method. That method seemed like the perfect way to ensure that all of my bindings updated properly. Though I came across cautionary tales, I did not heed the warnings—the method seemed to work so well.

My code was soon littered with $scope.$apply(). Then my app’s performance began to take a hit—transitions slowed, animations lagged. As a test, I commented out all instances of $scope.$apply(), and discovered that the app did run noticeably better. My perfect solution did not seem so rosy anymore.

In programming, it’s tempting to implement the first solution you come across without fully understanding how it works. In doing so, you open yourself up to all the side effects of the solution. In some cases, those side effects are minuscule and harmless. Others will sorely impede your program. There are always compromises to every solution—in memory, in performance—and it’s up to you as the programer to understand what is happening underneath the hood in order to make educated decisions.

With that in mind, I set out to understand how exactly $scope.$apply() works. I highly recommend Sandeep Panda’s post for a primer.

Here is the short answer—AngularJS features two-way data binding. This means that changes in the view (what you see) automatically reflect changes in the scope (the model or data), and vice versa. AngularJS accomplishes this by setting up “watchers” that are responsible for observing the changes.

Angular regularly performs $digest cycles, where the scope examines all of the $watch expressions and compares them with the previous value. Some directives automatically trigger a $digest cycle (for instance, anything that triggers the $apply method triggers a $digest cycle), but other times, changes go unnoticed.

I read many wonderful posts on how to clean up my code, but like the hacker I am, I decided to implement just enough optimizations so my app ran smoothly.

First, I removed every instance of $scope.$apply() where a watcher is already being fired. For instance, anytime a variable in the view is bound to the model (using brackets {{variable}}), a watcher is set up. I could safely remove $scope.$apply() that then set up duplicate watchers.

Second, in instances where changes were triggered by a direct user action, I added ng-change (credit to Ben Lesh for the idea).

From Ben Lesh.

Finally, I changed some of the remaining $scope.$apply() to $scope.$digest(). This is a small but effective optimization. While the $apply() method triggers watchers on the entire $scope chain, the $digest() method triggers watchers only on the current $scope and its children. If a local watch is sufficient, $scope().$digest() can safely replace $scope.$apply(). This optimization doesn’t apply to all cases. Ben Nadel offers sound guidelines on when and how to use this technique.

For my purposes, these few changes were more than enough to bring my app’s performance back to the level I wanted it to be. I’m always looking for feedback or improvements—please leave your thoughts in the comments below.

More Resources: