Sunday, January 26, 2014

Small performance tuning technique in AngularJS.


On my recent experience on a hybrid application development using angularJS and as a constant effort to give my user a seamless experience, I have come up with a small technique to optimize performance in the application. Its small but very effective and easy to implement. 

I expect the reader to have some working knowledge on angularJS. In case you don't have, then there are chances that you wont get the point.

Every small or big application contains some sort of list to show and when user clicks on the row item, the row has to be shown highlighted. Some thing like this. 


Now to achieve this in angular we will be using ng-class directive which is pretty cool feature provided by angular. something like this 

HTML

<div ng-app="myApp" ng-controller="AppCtrl">
      <div ng-repeat="child in data.children" 
            ng-class="{ selected : child.data.id === selectedId}"
            ng-click="setSelected(child.data.id)"  class="row">
        {{child.data.title}}
        <div>
          <img src="{{child.data.thumbnail}}"/>
        </div>
      </div>
    </div>

Script

angular.module('myApp', [])
.controller("AppCtrl" , function($scope){
  $scope.data = DUMMY_DATA.data;
  console.log($scope.data);
  $scope.setSelected = function(id){
    $scope.selectedId = id ;
  };
});

The entire code is also available here.

AngularJS Batarang is a great tool to analyze the performance of your angularJS based application. 

Lets see what does till tool shows when we use the default technique. The performance is something like this


Its says the code ng-class="{ selected : child.data.id === selectedId}"  , which is doing the highlighting magic is being watched by the framework and it takes 8.00 Ms of time. The list contains just 25 items. This will increase with the number of items in the list. What it does is, it's constantly watching for the selectedId property which is set inside the controller using setSelected(). Its called when user hit a row and id is set in the selectedId. Cool but have performance impact. 

Let's see the alternative using the directive.

<div ng-app="myApp" ng-controller="AppCtrl" row-highlighter="row">
      <div ng-repeat="child in data.children" 
             class="row" ng-click="setSelected(child.data.id)">
        {{child.data.title}}
        <div>
          <img src="{{child.data.thumbnail}}"/>
        </div>
      </div>
    </div> 

// Code goes here

angular.module('myApp', ['pasvaz.bindonce'])

.controller("AppCtrl" , function($scope){
  $scope.data = DUMMY_DATA.data;
  console.log($scope.data);
  $scope.setSelected = function(id){
    $scope.selectedId = id ;
  };
})

.directive("rowHighlighter",function(){
return {
link : function($scope,$element,$attrs){
var targetClass = $attrs.rowHighlighter;
var lastSelectedElement;
$element.bind("click" , function(event){
var currentTarget = event.target;
var found = false;
while(!currentTarget.classList.contains(targetClass)){
currentTarget = currentTarget.parentElement;
found = true;
}
if (found){
if (lastSelectedElement){
lastSelectedElement.classList.remove("selected");
}
currentTarget.classList.add("selected");
lastSelectedElement = currentTarget;
}
});
}
};
});

I have created a directive called rowHighlighter which attaches a click event on the list and will set the selected row. Lets see what batarang has to analyze.


Its not watching ng-class="{ selected : child.data.id === selectedId}" . It's just watching the elements it has to render in the row. Cool huh. Lets dig in the directive link

                 var targetClass = $attrs.rowHighlighter;
var lastSelectedElement;
$element.bind("click" , function(event){
var currentTarget = event.target;
var found = false;
while(!currentTarget.classList.contains(targetClass)){
currentTarget = currentTarget.parentElement;
found = true;
}
if (found){
if (lastSelectedElement){
lastSelectedElement.classList.remove("selected");
}
currentTarget.classList.add("selected");
lastSelectedElement = currentTarget;
}
});

There is only one click event listener for the entire list. Whenever any click happens anywhere inside the row, It selects the element whose background color has to set with the highlighted color. In this case element which has "row" class is the row which has to be highlighted. and then it keep a pointer to it. When user hits another row again then first it removes the last highlighted row and then add the color to the current one.

That's how I am able to remove the number of watch expression to make it more responsive. I have found another great module called bindonce which is really useful if you have a list which is just read only and dont need the data binding for the list. I have the example using bindonce.

using bindonce and ng-class


using bindonce and the directive



I find bindonce to be very effective. Try out and see what difference it brings to your application.