Angular plus React equals Speed revisited

Speeding up Angular 1.x demo to be on par with React

Recently I was lucky to attend ng-conf 2015 where I watched an interesting presentation by Dave Smith @djsmith42 "Angular + React = Speed". You can watch the presentation online at https://youtu.be/XQM0K6YG18s. During the presentation Dave was live programming and forgot to take average delay of 250ms from the Angular 1.x and React demos, making it unintentionally appear as if React is slower than the third Angular 2.0 implementation. The comments under the video explain the mistake, and you can play with Angular 1.x / React implementation by trying the code yourself.

I downloaded the code and took a quick look. The example has 2 calendars, first implemented using Angular 1.x, and the second using React widget included from an Angular wrapper.

TL;DR I measured each application's performance and optimized the Angular 1.x performance; the improved version runs in 700ms (compared to original 15 seconds), making it comparable to React.

Calendar screenshot

Each of the 2 examples shows the same feature: a table implemented as a custom widget that shows a calendar. Each cell of the calendar is a custom cell widget. You can click on a cell and it simulates an AJAX request to the server to fetch number of available time slots. For example I clicked on 3 cells.

Plan of attack

During the presentation, the React blew Angular 1.x implementation out of the water. I was very successful in my work improving an angular app's speed and wanted to investigate this very bad performance a little closer. In this blog post I will:

  • describe the Angular 1.x application
  • measure its performance and will point some obvious bottlenecks
  • measure the React implementation's performance
  • improve the Angular 1.x application performance by removing the global digest cycle

To better compare the implementation I clamped the synthetic timeout delay to 0ms, instead of the initial 10 seconds. This makes the model (scopes) and view updates the main application bottlenecks.

Angular 1.x code

The code to render vanilla Angular 1.x custom directive is below (the code uses ES6 string templates)

angular.module('myapp', [])
  .directive("myCalendar", function() {
    return {
        restrict: 'E',
        scope: true,
        replace: true,
        template:`
          <div>
           <button class="btn" ng-hide="loaded" ng-click="load()">Load</button>
           <button class="btn" ng-show="loaded" ng-click="searchAll()">Search all month</button>
           <table ng-if="loaded">
            <tr>
             <th ng-repeat="day in days" class="day-header">
               {{day}}
             </th>
            </tr>
            <tr ng-repeat="hour in hours">
             <td ng-repeat="day in days" class="hour-cell">
               <my-calendar-cell hour="{{hour}}" day="{{day}}"></my-calendar-cell>
             </td>
            </tr>
           </table>
         </button>
         `,
        link: function(scope, element, attrs) {
            scope.loaded = false;
            scope.hours = _.range(24);
            scope.days = DAYS;
            scope.searchAll = function() {
              scope.$broadcast('allSearchRequested');
            }
            scope.load = function() {
              scope.loaded = true;
            }
        }
    }
})

The calendar is simply a nested ng-repeat list

1
2
3
4
5
<tr ng-repeat="hour in hours">
<td ng-repeat="day in days" class="hour-cell">
<my-calendar-cell hour="{{hour}}" day="{{day}}"></my-calendar-cell>
</td>
</tr>

The "Search all month" button executes a broadcast to all cells

1
2
3
scope.searchAll = function() {
scope.$broadcast('allSearchRequested');
}

The individual calendar cell code is similarly simple

directive("myCalendarCell", function() {
  return {
    restrict: 'E',
    replace: true,
    scope: true,
    template: `
      <div ng-click="cellClicked(day, hour)" ng-class="cellClass()">
        <div ng-if="showHour()" class="time">
          {{hour}}:00
        </div>
        <div ng-if="showSpinner()">
          ...
        </div>
        <div ng-if="showSearchResults()">
          <div>{{status.searchResults.options}}</div>
          <div class="result-label">results</div>
        </div>
      </div>
      `,
    link: function(scope, element, attrs) {
      scope.day = attrs.day;
      scope.hour = attrs.hour;
      scope.status = {};
    },
    controller: function($scope, $rootScope, $timeout) {
      $scope.showSpinner = function() {
        return $scope.status.isSearching;
      }
      $scope.showHour = function() {
        return !$scope.status.isSearching && !$scope.status.searchResults;
      }
      $scope.showSearchResults = function() {
        return $scope.status.searchResults;
      }
      $scope.cellClass = function() {
        if ($scope.status.isSearching) {
          return 'searching';
        } else if ($scope.status.searchResults) {
          if ($scope.status.searchResults.options > 3) {
            return 'good-results'
          } else if ($scope.status.searchResults.options > 1) {
            return 'weak-results'
          } else {
            return 'bad-results'
          }
        }
      }
      $scope.cellClicked = function() {
        delete $scope.status.searchResults;
        $scope.status.isSearching = true;
        // Simulate an AJAX request:
        $timeout(function() {
          $scope.status.isSearching = false;
          $scope.status.searchResults = {options: Math.floor(Math.random() * 5)};
        }, randomMillis());
      }
      $scope.$on('allSearchRequested', function() {
        $scope.cellClicked();
      });
    }
  }
})

We can trigger the search by clicking the cell or by receiving an even allSearchRequested

1
2
3
<div ng-click="cellClicked(day, hour)" ng-class="cellClass()">
...
</div>
1
2
3
4
5
6
7
8
9
10
11
12
$scope.cellClicked = function() {
delete $scope.status.searchResults;
$scope.status.isSearching = true;
// Simulate an AJAX request:
$timeout(function() {
$scope.status.isSearching = false;
$scope.status.searchResults = {options: Math.floor(Math.random() * 5)};
}, randomMillis());
}
$scope.$on('allSearchRequested', function() {
$scope.cellClicked();
});

Angular 1.x performance

I am running the examples using angular 1.4.0-beta.6. The calendar's performance is atrocious. The "Search all month" takes around 15 seconds!

Angular 1.x cpu profile

Using ng-count-watchers code snippet I counted 5272 watchers in this page. The majority come from this Angular 1.x calendar.

individual cell template
1
2
3
4
5
6
7
8
9
10
11
12
<div ng-click="cellClicked(day, hour)" ng-class="cellClass()">
<div ng-if="showHour()" class="time">
{{hour}}:00
</div>
<div ng-if="showSpinner()">
...
</div>
<div ng-if="showSearchResults()">
<div>{{status.searchResults.options}}</div>
<div class="result-label">results</div>
</div>
</div>

The watchers are created for these expressions

  • ng-class="cellClass()"
  • ng-if="showHour()"
  • :00
  • ng-if="showSpinner()"
  • ng-if="showSearchResults()"

Each cell has 6 watch expressions, and there are 720 cells, making it every expensive table to update. The idle digest cycle runs for about 14ms measured using ng-idle-apply-timing.js.

While this is not too bad, the fact that we have to rerun it on every cell update means we are running the entire digest cycle many times. Using ng-count-digest-cycles.js code snippet I observed 1490 digest cycles during the calendar computation.

React performance

React implementation takes around 700ms to generate the values and update the calendar. You can see how fast it performs from the screen capture below

React update

Here is the CPU profile

React update CPU profile

Improving Angular 1.x performance by removing global scope apply

The simplest bottleneck to remove was to stop rechecking all cells whenever single cell fetched its own data. The current code simulated an Ajax request using $timeout call that triggers global digest cycle. Angular 1.4 has a new parameter in $timeout, and we simply pass false to avoid automatic $rootScope.$apply() after we get fake data back.

1
2
3
4
5
6
7
8
9
10
11
12
13
$scope.cellClicked = function() {
delete $scope.status.searchResults;
$scope.status.isSearching = true;
// Simulate an AJAX request:
$timeout(function() {
$scope.status.isSearching = false;
$scope.status.searchResults = {options: Math.floor(Math.random() * 5)};
$scope.$digest();
}, randomMillis(), false); // false - do not invoke $scope.$apply
}
$scope.$on('allSearchRequested', function() {
$scope.cellClicked();
});

Instead of automatically calling $rootScope.$apply() at the end of the $timeout we manually call $scope.$digest() that only updates the current cell's scope (any any children scopes).

1
2
3
4
$timeout(function() {
...
$scope.$digest();
}, delay, false);

This is a LOT more efficient. Let us measure the performance of this change. Remember, at the end of each request we do NOT check every watch expression in the entire calendar. Instead we update the scope object and run the digest cycle in the current cell and update its DOM via two-way binding.

Local digest CPU profile

BOOM! 650ms. The calendar updates as quick as the React version. 3 word change. The global digest cycle ran only twice.

Discussion

The original example repo has a pull request #2 where Lucas Galfasó uses the same approach (local digest cycle) + one time bindings to speed up the Angular 1.x version. Dave Smith rejects running the local digest cycle because this is not an option when executing $http requests.

Your most significant change is the localApply = false for the call to $timeout with a local digest. I specifically chose not to do that for this demo because this was meant to simulate an AJAX request with $http, which does not provide a localApply feature.

First, the current beta version of Angular 1.4 already adds something similar to this in the way of $http.useApplyAsync(true) that combines multiple scope.$apply calls from $http service into one. I feel a full support option for skipping the root scope apply call could be added next.

Second, Angular is a pretty flexible framework. Any data fetch mechanism used from a React application (remember, React is just a View library, so it needs something extra to actually fetch the data) could be easily used from an Angular app too. For example you can use fetch API, which replaced the painful XMLHttpRequest mechanism.

I always find two bottlenecks in my angular applications: my code and the digest cycle. I can optimize the problems in my code using CPU profiler, Chrome DevTools for example warns me about functions that cannot be optimized for some reason (like having try-catch block). To improve the digest cycle I can minimize number of expensive watchers, for example by precomputing filtered results, or by using one way data binding.

Angular digest cycle and the prototyping speed

In general I believe that Angular gives me a great power to develop any application in three simple steps

  • Create static page prototype with most HTML layout and CSS styles applied
    • I can show the page to other people to get feedback and quickly get to the look I need.
    • Most of this is done by the designer more familiar with the HTML / CSS tools rather than web programming.
  • Add several directives to get live data via two-way binding.
    • The static page suddenly becomes a live web application.
    • The built-in scopes and directives are simple enough for people without programming experience to use
    • This step is used again to refine the desired application features
  • Improve the performance by removing some degrees of freedom
    • This is where I would come in as a developer
    • Replace the flexibility of two-way data binding with more performance one-way binding
    • Replace global application digest cycle with local ones. The data flow is known and individual pieces can be isolated
    • Stop attaching everything to the scope and use controller instances instead, see Separate model from view

The beauty of step 1 and 2 is the simplicity and the fact that it just works. Yes, comparing plain scope objects when performing dirty checking is slow. But optimizing this operation before knowing what the final features seems a little premature to me, just like putting a concrete path before one knows where the people would walk.

desired paths

Angular team is working on replacing the plain scope objects with their graphs of relationships with trees of data, where the data flows are acyclical (watch Victor Savkin's Change detection reinvented talk). While this will lead to faster performance we will loose some of the "it just works" magic I like about using Angular to prototype. Instead I would prefer to be able to isolate the digest cycle to subcomponents. Imagine the follow data flow

combined digest flow

Inside the custom directives we have the current dirty checking because a scope property is allowed to access any other local scope property. Between the components we can use unidirectional data flow without cycles (for example by broadcasting events). Each custom directive's scope is isolated from other directives' scopes. This should speed up the global digest cycle while leaving each directive simple to program.

Disclaimer: When talking about the application development speed in the steps 1 and 2, I am talking about a prototype coded by a design-inclined web developer. I love the immutable data and functional javascript myself (as a proof, see all my blog posts on functional programming). I just think that creating a live web app using Angular provides a more visual and intuitive path than writing JSX code! For example, see Just Enough Angular for Designers tutorial - I honestly believe a designer who can code static site in HTML and CSS can turn it into a web app with ease.