AngularJS may have a lot of advantages over other frameworks for building front end applications but it can have performance problems when an app starts to get large.
There can be several reasons an AngularJS application may be slowing down. Recently during our internal project which was built in angular, we faced a lot of performance issues as the application grew in size and complexity. We took many steps to optimize our application and make it execute without any performance lag. Some of the approaches are discussed below.
Reduce Watchers
We had lot of 2 way bindings in our project and we binded every model both ways in our view. But as the app grew in size the $$watcher population increased significantly and started hampering app performance. Angular uses dirty checking approach to keep track of the changes in models and to reflect them in the view (bi-directional data binding). This means it will have to go through every $$watcher to check if they need to be updated (call the $digest cycle).
You should be mindful that the number of bi-directional data bindings should not exceed more than 2000 data bindings on the page for each $digest loop.
One-time binding syntax {{ ::value }}
Angular Js 1.3 came up with a feature which is the ability to bind data to the view without adding a watcher. This feature greatly helped us to reduce watchers. We used this extensively on all those variables which needed to be rendered in the view only once.
In the above code first < p> tag will not get affected on clicking the button as it one time binded using ‘::’, whereas the second <p> tag will be updated every time the button is clicked.
Angular sets $$watchers on below mentioned points and we should use them wisely
- $scope.$watch
- {{ }} type bindings
- Most directives (i.e. ng-show)
- Scope variables scope: { bar: ‘=’}
- Filters {{ value | myFilter }}
- ng-repeat
$$watchers (digest cycle) run on
- User action (ng-clicketc). Most built in directives will call $scope.$apply upon completion which triggers the digest cycle.
- ng-change
- ng-model
- $httpevents (so all ajax calls)
- $qpromises resolved
- $timeout
- $interval
- Manual call to $scope.$applyand $scope.$digest
ng-repeat
As we had many lists in our application, we used ng-repeat extensively with deep nested objects as well. But the page started to lag after the array of the list started to get complex.
ng-repeat directive is one of the biggest sources of delay in Angular, it is a powerful directive of angular and often easy to abuse and misuse. Handling ng-repeat inefficiently will create too many $$watchers.
We took some steps to efficiently use ng-repeat which were very fruitful.
ng-repeat track by
ng-repeat has an attribute “track by” with which we can supply a unique id. This reduces the number of dom repaints and dirty checking needed.
ng-repeat filter
Avoid using ng-repeat filter, it is better to pass a filtered array to ng-repeat than using filter.
ng-repeat function
Avoid using function to return the array to be used for ng-repeat. These functions need to be recalculated even when there is no change, unnecessarily reloading the ng-repeat each time.
$watchCollection instead of $watch
Angular supports a third parameter in $watch method which allows deep checking the object, which means to check every property of the object, which can be quite expensive.
$watch(‘value’, function(){}, true)
To address this performance issue we used $watchCollection which works same as $watch with 3 parameters, except only that it checks only the first layer of the object properties.
$watchCollection(‘value’, function(){})
Debouncing ng-model
We had some filters, validations and api calls on the input fields which were called on every key press and it triggered the digest cycle causing Angular to update all $$watchers and bindings in the app to see if anything has changed. We used debounce option on ng-model which helped us to greatly reduce the server load, filter calls and eventually the digest loop calls.
Debouncing is a method to apply changes to the model when a user stops typing for a specified amount of time. This method can significantly improve the user experience as the user will not experience lag due to $digest loop while typing.
ng-if instead of ng-show
ng-show and ng-hide uses css ‘display’ property to show hide elements which means the node is always present in the dom and is a part of calculations done in every $digest cycle. This is only useful if we are toggling a particular node very often. For all the other cases we used ng-if can as it removes the node from the dom, which reduces unnecessary calculations.
Optimizing filters
Initially in our app we used many filters inline in the view. But as the number of filters increased the number of $digest cycles calls also increased as every filter in the view is called a minimum of two times per $digest cycle. So we moved all filters in our javascript which were required to be rendered in the view only once. This pre-processes our data before sending it to the view, which avoids the step of parsing the DOM.
That is to say that, rather than using the currency filter in the view, we can apply the filter to the model when we retrieve the data in the controller or service.
That is to say instead of using the filter in the view, like so: