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.

Wednesday, December 25, 2013

Why we Choose Angular Over Ember


By any chance I am not meaning that I dont like EmberJS over AngularJS. With my limited knowledge, I love both the javascript framework. I think based on the application, you can use. 


Brief about our application
Its a small hybrid single page application which gets the data from the server and renders on the UI. Our target form factors are tablets and later support for mobiles. Mostly the application is about getting list and displaying the details of the item selected by the user. There is not much form at this point of time. Screens will be rendered based on Routing model.

In order to finalize the framework which will suit our need we created a small POC which fetches the JSON data including images URL and renders that list on the screen. we created this application using ember and angular and was amazed to see that angularJS renders data much faster than emberJS. Since targets are tablets and mobiles, 500 ms time also matters to the user experience.


Code is given below for both JS Framework. Its using JSON as data source and it contains around 520 childrens. Based on this data set, Ember is taking 2000 MS more time to render all the records in the desktop. And Its too high when we consider mobile and  tablets. Based on the requirement of our project, finally we decided to go with angular.

I would advice to consider all nuts and bolts of your application before choosing a framework. Create a POC and check the performance and other aspects such synchronization of data...

EmebrJS

index.html Code

<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>Ember Starter Kit</title>
</head>
<body>
<h1>Ember Demo App</h1>
  <script type="text/x-handlebars">
 {{outlet}}
  </script>
  
  <script type="text/x-handlebars" data-template-name="index">
{{#each}}
 <div style="background-color:#CDD9E4;margin:10px;">
  <div>{{data.domain}}</div>
  <div>{{data.selftext}}</div>
  <div>{{data.author}}</div>
  <div>{{data.clicked}}</div>
  <div>{{data.sticked}}</div>
  <div>{{data.permalink}}</div>
  <div>{{data.title}}</div>
  <div>{{data.url}}</div>
  <div>{{data.num_comments}}</div>
  <div>{{data.visited}}</div>
  <div>{{data.num_reports}}</div>
  <div>{{data.ups}}</div>
 </div>
{{/each}}
  </script>
  
  <script src="js/libs/jquery-1.10.2.js"></script>
  <script src="js/libs/handlebars-1.1.2.js"></script>
  <script src="js/libs/ember-1.2.0.js"></script>
  <script src="js/app.js"></script>
</body>
</html>

app.js file

App = Ember.Application.create();

App.Router.map(function(){
this.resource("index",{path:"/"});
});

App.IndexRoute = Ember.Route.extend({
  model: function() {
    return Ember.$.getJSON("../data/data.json").then(function(data){
    return data.data.children;
    });
  }
});


Angular JS File

index.html

<!DOCTYPE html>
<html ng-app="myApp">
<head>
<meta charset="utf-8">
<title>Angular Starter Kit</title>
</head>
<body>
<h1>Angular Demo App</h1>
 
 <div ng-view></div>
 
  <script src="js/lib/angular.js"></script>
  <script src="js/lib/angular-route.js"></script>
  <script src="js/app.js"></script>
</body>
</html>

app.js file
var App = angular.module('myApp',['ngRoute'])
.config(['$routeProvider' , function ($routeProvider){
$routeProvider.when("/",{
templateUrl : "template/home.html",
controller : "HomeController"
});
}]).controller("HomeController" , ["$scope" , '$http' , function($scope,$http){
$http.get("../data/data.json").success(function(data){
$scope.children = data.data.children;
});
}]);

home.html


<div style="background-color:#CDD9E4;margin:10px;" ng-repeat="data in children">
<div>{{data.data.domain}}</div>
<div>{{data.data.selftext}}</div>
<div>{{data.data.author}}</div>
<div>{{data.data.clicked}}</div>
<div>{{data.data.sticked}}</div>
<div>{{data.data.permalink}}</div>
<div>{{data.data.title}}</div>
<div>{{data.data.url}}</div>
<div>{{data.data.num_comments}}</div>
<div>{{data.data.visited}}</div>
<div>{{data.data.num_reports}}</div>
<div>{{data.data.ups}}</div>
</div>

data.json

{
  "kind": "Listing",
  "data": {
    "modhash": "",
    "children": [
      {
        "kind": "t3",
        "data": {
          "domain": "self.cute",
          "banned_by": null,
          "media_embed": {
            
          },
          "subreddit": "cute",
          "selftext_html": "&lt;!-- SC_OFF --&gt;&lt;div class=\"md\"&gt;&lt;p&gt;No videos or websites, please. If your post is not an image it will be removed.&lt;/p&gt;\n&lt;/div&gt;&lt;!-- SC_ON --&gt;",
          "selftext": "No videos or websites, please. If your post is not an image it will be removed.",
          "likes": null,
          "secure_media": null,
          "link_flair_text": null,
          "id": "1ms27c",
          "secure_media_embed": {
            
          },
          "clicked": false,
          "stickied": true,
          "author": "HairyRainDrop",
          "media": null,
          "score": 19,
          "approved_by": null,
          "over_18": false,
          "hidden": false,
          "thumbnail": "self",
          "subreddit_id": "t5_2qh5l",
          "edited": false,
          "link_flair_css_class": null,
          "author_flair_css_class": null,
          "downs": 8,
          "saved": false,
          "is_self": true,
          "permalink": "/r/cute/comments/1ms27c/just_a_reminder_images_only/",
          "name": "t3_1ms27c",
          "created": 1379691227.0,
          "url": "http://www.reddit.com/r/cute/comments/1ms27c/just_a_reminder_images_only/",
          "author_flair_text": null,
          "title": "Just a reminder: Images only",
          "created_utc": 1379687627.0,
          "ups": 27,
          "num_comments": 6,
          "visited": false,
          "num_reports": null,
          "distinguished": "moderator"
        }
      }
    ],
    "after": "t3_1tbtkp",
    "before": null
  }
}

Repeat the chilren object... or download the JSON from http://www.reddit.com/r/cute/.json.