Saturday, April 26, 2014

Why is AngularJS slower with jQuery?

(All data in this blog post is based on profiling on an iPhone5 with iOS 7.1)

TL;DR: AngularJS's JQLite has significantly faster .data() and .text()

In jQuery makes AngularJS 50% slower on an iPhone I showed the performance difference between AngularJS with jQuery and AngularJS without jQuery. That 50% was with AngularJS 1.2 and jQuery 1.11 - with AngularJS 1.3 (beta6) and jQuery 2.1 the difference was 35%.

I did a bit of testing to see if there was a simple reason for the slowdown, and turns out that there is. With two changes I got the difference down to 8% (created using The Mobile HTML5 Rendering Profiler; click for a large view):

The changes were to comment out most uses of .data() from AngularJS and changing ng-bind to use a simple inlined code to change the element text instead of jQuery's .text(). Note that these are not 'fixes' that can be applied to AngularJS - they don't preserve all the functionality. What they are is a way to see what is causing the slowdown. Here's the diff:

--- angular-1.3.0-beta.6.js 2014-04-26 16:02:35.000000000 +0300
+++ angular-1.3.0-beta.6-hacked.js 2014-04-26 15:57:23.000000000 +0300
@@ -6007,7 +6007,7 @@
           : $compileNodes;
 
         forEach(transcludeControllers, function(instance, name) {
-          $linkNode.data('$' + name + 'Controller', instance);
+          //$linkNode.data('$' + name + 'Controller', instance);
         });
 
         // Attach scope only to non-text nodes.
@@ -6015,7 +6015,7 @@
           var node = $linkNode[i],
               nodeType = node.nodeType;
           if (nodeType === 1 /* element */ || nodeType === 9 /* document */) {
-            $linkNode.eq(i).data('$scope', scope);
+            // $linkNode.eq(i).data('$scope', scope);
           }
         }
 
@@ -6105,7 +6105,7 @@
           if (nodeLinkFn) {
             if (nodeLinkFn.scope) {
               childScope = scope.$new();
-              $node.data('$scope', childScope);
+              // $node.data('$scope', childScope);
             } else {
               childScope = scope;
             }
@@ -6572,9 +6572,9 @@
           isolateScope = scope.$new(true);
 
           if (templateDirective && (templateDirective === newIsolateScopeDirective.$$originalDirective)) {
-            $linkNode.data('$isolateScope', isolateScope) ;
+            // $linkNode.data('$isolateScope', isolateScope) ;
           } else {
-            $linkNode.data('$isolateScopeNoTemplate', isolateScope);
+            // $linkNode.data('$isolateScopeNoTemplate', isolateScope);
           }
 
 
@@ -6677,7 +6677,7 @@
             // later, once we have the actual element.
             elementControllers[directive.name] = controllerInstance;
             if (!hasElementTranscludeDirective) {
-              $element.data('$' + directive.name + 'Controller', controllerInstance);
+              // $element.data('$' + directive.name + 'Controller', controllerInstance);
             }
 
             if (directive.controllerAs) {
@@ -18914,12 +18914,13 @@
    </example>
  */
 var ngBindDirective = ngDirective(function(scope, element, attr) {
-  element.addClass('ng-binding').data('$binding', attr.ngBind);
+  element.addClass('ng-binding'); // .data('$binding', attr.ngBind);
   scope.$watch(attr.ngBind, function ngBindWatchAction(value) {
     // We are purposefully using == here rather than === because we want to
     // catch when value is "null or undefined"
     // jshint -W041
-    element.text(value == undefined ? '' : value);
+    element[0].textContent = (value == undefined ? '' : value);
+    // element.text(value == undefined ? '' : value);
   });
 });

The test is comparing The version without jQuery to the version that loads jQuery before AngularJS; both with the above changes to use of .text() and .data(). Commenting out .data() took the difference from 35% to 20%, and changing .text() from 20% to 8%.

This simple test only really uses ng-repeat and ng-bind. Although these are important parts of AngularJS, a typical application will quite likely use ng-class, ng-if, ng-switch, ng-show etc. These might also show similar slowdowns with jQuery.

It would be interesting to try and optimize these two methods in jQuery, implementing them along the lines of what AngularJS does. I'm not really familiar enough with jQuery to know if there are some real semantic differences in how they are supposed to work (in comparison to AngularJS).

Oh, and I did first think there might be a difference in Garbage Collection, but that was clearly not the case (this is a Timeline image from running with jQuery, there is no GC drop in the memory usage during the rendering, only on the page reload):

Note that the Profiler lets you see Timelines from the iPhone in the (better) Chrome Developer Tools.

3 comments:

Dave Methvin said...

For .text(), I suspect Angular is faster because it doesn't clean up after any elements removed from the document as a result of setting text on the element. That's potentially a bug in Angular.

For .data(), Angular does not get data attributes which makes it faster but not compatible with jQuery, see https://github.com/angular/angular.js/issues/3538 .

Mika Raento said...

Thanks @Dave Methvin - that's a great answer to my wondering above!

Geetha Devi said...

Given so much info in it, These type of articles keeps the users interest in the website, and keep on sharing more ..Best Angularjs Training in Chennai|Angularjs Training in Chennai