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.

Friday, April 25, 2014

On Android, jQuery makes AngularJS (only) 20% slower

The previous blog post showed that adding jQuery to AngularJS made it 50% slower on an iPhone. I ran the same test on the now-released! Android version of the Profiler (topmost result comes from loading jQuery before AngularJS and bottom from not loading it - the loading time is not part of the times shown; click for larger view):

On Android the slowdown is only 20%, which is nice. What's not so nice, and a bit puzzling, is that Android 4.4 / Chrome 34 / Samsung S5 is over 100% slower than iPhone5/iOS7:

I have to admit that I haven't looked at other performance comparisons between iPhone5 and the S5.

The Mobile Safari Rendering Profiler

To delight users, your mobile app needs to feel snappy and respond to gestures immediately. Doing that with the mobile web can be tough. The Mobile Safari Rendering Profiler helps you buy measuring whether your changes are helping. It runs your test setup several times so that you can see distinguish real changes from noise caused by timing, javascript optimization and garbage collection.

Test setup details

The iPhone test was run on an iPhone5 running iOS 7.1 (and Mobile Safari). The Android device is a Samsung Galaxy S5, running Android 4.4.2 and Chrome 34. The test runs after the page has been loaded and initial contents rendered, it then measures the time to generate 2000 items using ng-repeat. The test was run with AngularJS 1.2.16 and jQuery 1.11.

I did also run the comparison on desktop Chrome 34. There the difference between plain AngularJS and AngularJS-with-jQuery is only 6%.

Thursday, April 24, 2014

jQuery makes AngularJS 50% slower on an iPhone

AngularJS can optionally use jQuery instead of its built-in jqLite DOM library. This is useful if you want to integrate jQuery-based components into your app. From previous testing I knew jQuery was somewhat slower, but I have to admit I was surprised when I found out how much slower it was.

The test is a very simple AngularJS app with a button that creates 2000 items rendered with ng-repeat. Below is a screenshot of profiling this with my Mobile HTML5 Rendering Profiler - the version with jQuery 1.11 spends over twice as much time in Javascript and a bit more time in Style calculation (click for larger view):

The Mobile HTML5 Rendering Profiler

To delight users, your mobile app needs to feel snappy and respond to gestures immediately. Doing that with the mobile web can be tough. The Profiler helps you buy measuring whether your changes are helping. It runs your test setup several times so that you can see distinguish real changes from noise caused by timing, javascript optimization and garbage collection.

Updating the visible UI of your web app requires changing the DOM (with javascript), recalculating the styles and layout and painting the results on-screen. The Profiler will show you the time taken by each phase, as well as how many recalculations are needed for the end results. These will guide you in making changes that improve the visible performance.

The Profiler will set you back 55 EUR (+ VAT), but do first download the 7-day trial and see what makes your app go fast (or slow...).

Test setup details

This uses angular 1.3.0-beta6 and jQuery 1.11.0, running on an iPhone5 with iOS 7.1.1. The profiling is done for the button press, so after the javascript has been fully loaded (but probably before significant optimizations of the javascript). The pages under test are plain AngularJS and with jQuery - the only difference is in loading or not loading jQuery (before Angular).

jQuery 2.1: better

jQuery 2.1 improves things a bit, but is still 35% slower:

AngularJS 1.2: the same story

(Update at 2014-04-24 21:17 EEST)

@m_gol on Twitter rightly pointed out that AngularJS 1.3 and jQuery 1.11 isn't the most relevant combination as AngularJS 1.3 has dropped IE8 support whereas jQuery 1.11 hasn't (but jQuery 2.1 has). Now that isn't the whole story, as the Angular folk are saying that 1.3 might still work with IE8, but it's a good point.

Here's then the same test but using AngularJS 1.2 (1.2.16) instead of 1.3. Now jQuery is 56% slower:

Android: not as bad, and yet, worse.

(Update on 2014-04-25) See the next blog post.

Why is AngularJS slower with jQuery?

(Update on 2014-04-26) See the followup.