Newer Post

Setting Up JavaScript Testing Tools for ES6

Older Post

6 Ways to Help You Find Inspiration

Performant Angular with Redux and Immutable

Abstract

The angular JS framework has become ubiquitous in the front end world. And with good reason. It allows you to abstract concepts such as rendering, it helps you manage dependencies and modules (which, granted, has become almost a drawback in this post ES6 world), and makes it easy to built all but the most involved of components. But frequently, as an application grows in size and complexity, we begin running into problems. They come in many flavors, but the most damning and the one on which we will spent the majority of this article is performance. Simply put, as the number of distinct elements we are rendering grows, the AngularJS application begins to slow down, eventually rendering it nearly unusable. This article will attempt to explain how and why this happens, and then, armed with this knowledge propose a workaround that avoids having to rewrite the application entirely in either Angular 2 or React.

Target Audience

This article is aimed at AngularJS engineers with some real world experience. Some familiarity with AngularJS will be presumed, and the most simplistic concepts are not going to be reexplained. This article will also serve as an introduction to Redux/Immutable, and the Flux architecture in general.

Background information

The Digest Cycle

The angular digest cycle is the engine angular uses in order to propagate updates. At its core, it is an extension on the browser’s native event loop, which utilizes an array of watchers to track values to change, and dirty checking to track their changes. Watchers are added when something is bound to the angular scope with either inline bindings

<div> {{my_scope_value}} </div>

or by watchers directly

$scope.$watch('my_other_value', function(){})

Dirty checking is relatively straight forward. It is a process that allows angular to validate whether there is a value that has not yet synchronized through the whole application, and to synchronize it if that is the case. If a value that is being watched has changed, the angular digest cycle will apply dirty checking to every single other watched variable to ensure that none of them were updated be the watcher on the initial change. If anything comes back “dirty” or changed from the previous value, the entire digest cycle is rerun, until everything comes back “clean” or Angular throws an exception.While this approach is incredibly effective at finding and coalescing changes, it suffers a fairly self evident performance problem when there are many watchers in a given scope (large lists of data for example).

Flux architecture

The flux architecture was created at Facebook with the goal of building saner, more maintainable client side systems in React. However, being an architecture rather than a library, it lends itself fairly easily to developing client side systems in other frameworks, such as Angular. At its core it replaces the traditional MVC as represented by angular with its own set of ideas. Fundamentally, in a Flux system, there are three major parts. The view, the dispatcher, and the store. The store tracks the systems state and changes to it, the dispatcher receives and routes actions, which cause effects to happen to stores, such as fetching data from a server-side API, and the view renders the data in a User accessible way. In our setup, the view will be represented by angular directives.The big idea of flux, and what will really help us in overcoming the angular digest problem, is the idea of unidirectional data flow. In short, the view (angular directive), will never update the data directly. Instead, it will issue actions, which will cause the state as tracked by a flux store to change, allowing us to coalesce multiple changes in a single place. This will then update the view. As all changes are made in one shot, the initial digest cycle will mark everything as clean, and run once. Further actions are then dispatched as necessary.

Redux

Redux is one possible implementation of Flux. Redux differs from the vanilla Flux philosophy in two, fundamental ways. First, there is only one, singleton store, negating the need for a dispatcher. The Store can further have multiple Reducers, functions that operate on and modify specific subsections of the Store.

A simple example

To illustrate the problem with the digest cycle we are going to build a (trivial) application. Let’s say that we wish to be able to add and remove entries from a list of rendered fields.We could set this up to look something like so:

angular.module('demo').directive('listDirective', function () {  
  return {
    templateUrl: 'list.html',
    restrict: 'E',
    link: function(scope) {
      scope.list = [];
      scope.addItem = function() {
        scope.list.push({value: '', added: false});
      }
      scope.removeItem = function(index) {
        scope.list.splice(index, 1);
      }
    }
  }
})

angular.module('demo').directive('listItem', function () {  
  return {
    templateUrl: 'item.html',
    restrict: 'E',
    scope: { item: '=', remover: '=', removalIndex: '@' },
    link: function(scope) {
      scope.remove = function() {
        scope.remover(parseInt(scope.removalIndex));
      }
      scope.confirmItem = function() {
        scope.item.added = true;
      }
    }
  }
})

With the following two templates:

<list-item ng-repeat="item in list" item="item" remover="removeItem" removal-index="{{$index}}"></list-item>  
    <button ng-click="addItem()">Add Item</button>

And

    <div ng-if="item.added">{{item.value}}</div>
    <input ng-if="!item.added" ng-model="item.value"></input>
    <button ng-if="!item.added" ng-click="confirmItem()">Confirm Item</button>
    <button ng-if="item.added" ng-click="remove()">Remove Item</button>

This is intentionally a fairly trivial example, but it does serve to demonstrate two things:

1) Even this, very trivial application, has 13 user introduced watchers firing per dirty check, assuming a list length of 1, and will gain 8 new watchers every time an item is added to the list. While it is true that this could be somewhat improved upon it does serve to illustrate how quickly the digest cycle can become very expensive. Now if we imagine other parts of the system interacting with this data, and modifying things, we could have a potentially very expensive cycle operating repeatedly!

2) The interlocking nature of nested directives causes data to be hard to reason about, and inherently mutable (notice how we change item directly in the child directive).

You can view the running code of this example: here

Reduxifying our example

The first thing we need to do is to wrap Redux into an angular service for ease of injection:

angular.module('demo').service('Redux', function (){ return Redux; });  

We can then define our reducer and store as follows:

angular.module('demo').service('listReducer', function () {  
  return function(state = {list: []}, action) {
    switch (action.type) {
    case 'ADD_ITEM':
      state.list.push({value: '', added: false});
      return state;
    case 'REMOVE_ITEM':
      state.list.splice(action.index, 1);
      return state;
    case 'CONFIRM_ITEM':
      state.list[action.index].added = true;
      return state; default: return state;
    }
  }
});

angular.module('demo').service('applicationStore', ['Redux', 'listReducer', function (Redux, listReducer) {  
  var initialState = { list: [] };
  return Redux.createStore(listReducer, initialState);
}]); 

Finally we change our directive to consume and update our store:

angular.module('demo').directive('listDirective', function (applicationStore) {  
  return {
    templateUrl: 'list.html',
    restrict: 'E',
    link: function(scope) {
      applicationStore.subscribe(function (state) {
        scope.list = applicationStore.getState().list;
      })
      scope.addItem = function() {
        applicationStore.dispatch({ type: 'ADD_ITEM' });
      }
    }
  }
})
angular.module('demo').directive('listItem', function (applicationStore) {  
  return {
    templateUrl: 'item.html',
    restrict: 'E',
    scope: { item: '=', removalIndex: '@' },
    link: function(scope) {
      scope.remove = function() {
        applicationStore.dispatch({ type: 'REMOVE_ITEM', index: scope.removalIndex });
      }
      scope.confirmItem = function() {
        applicationStore.dispatch({ type: 'CONFIRM_ITEM', index: scope.removalIndex });
      }
    }
  }
}) 

There are several things worth noting here. First and foremost, is the fact that the store does all updates independently of the angular digest cycle. When we subscribe, we do still trigger an update cycle, however since our store is the only source of updates, this will always come back clean.

Additionally, since we only make data changes in a single place, our data should follow the “one way” flow of Flux, and by easier to reason about.

Effectively this turns AngularJS into the view layer utilizing its powerful directive concept as a unit of reuse and abstraction, while removing its “engine” in favor of a faster, more performant implementation.

The last thing worth noting, is that we could easily construct multiple reducers that we amalgamated in the store creation, and have respond to different actions.

The running application can be found: here

The case for Immutable

There is still one glaring flaw with this system. The code still has a shared mutable state. If we were to, for example, add multiple isolate scope reference bindings, such as our

list: '='

our application could get into a non linear state if a data update fails asynchronously, and potentially render an inconsistent UI, forcing us to reason about where a reference is consumed, and where this shared state could be broken.

Immutable.JS is a Javascript library that attempts to sidestep this problem by making things be Immutable objects, and generating new references on update.In service of that, we change our initialState to an Immutable:

var initialState = { list: Immutable.List([]) };  

convert back in our directive for ease of rendering:

applicationStore.subscribe(function () {  
  scope.list = applicationStore.getState().list.toJS(); 
}) 

convert our reducer so that it updates the immutable and generated fresh references:

function(state = {list: Immutable.List([])}, action) {  
  switch (action.type) {
  case 'ADD_ITEM':
    state.list = state.list.push(Immutable.Map({value: '', added: false}));
    return state;
  case 'REMOVE_ITEM':
    state.list = state.list.splice(action.index, 1);
    return state;
  case 'CONFIRM_ITEM':
    state.list = state.list.setIn([action.index, 'added'] ,true);
    return state;
  case 'MODIFY_ITEM':
    state.list = state.list.setIn([action.index, 'value'], action.value);
    return state default: return state;
  }
} 

And update our view to trigger the new MODIFY_ITEM action:

scope.change = function(localValue) {  
  applicationStore.dispatch({ type: 'MODIFY_ITEM', index: scope.removalIndex, value: localValue }) 
}
<input ng-if="!item.added" ng-model="item.value" ng-change="change(item.value)" ng-model-options="{ debounce: 1000 }"  

An important note is the ng-model-options which avoids an annoying flicker.

The important thing to understand is that when an Immutable instance receives a destructive update, such as .push or .splice, it returns a brand new reference rather than updating the existing object. As such, any objects in your system that have a reference to the old object, will not mutate until they receive the new reference.

You can view the final state here

Conclusion

Whether you want to move away from Angular entirely into a Flux enabled framework and wish to test the waters, or are happy with angular but want to short circuit the digest cycle for performance, this trick will help you achieve that goal at fairly low cost. There is a handful of gotchas (you should only ever have one store, and use multiple reducers to operate on different parts of its state tree), but the implementation above does address some of the more common problems of Angular development, and allows an Angular application to scale much further then vanilla Angular would permit.

We'll help you unleash.

Join the 20,000 developers who subscribe to our newsletter.

Scale your
Development team

We help you execute projects by providing trusted developers who can join your team and immediately start delivering high-quality code.

Hire Developers
javascript, code, angular, redux