Because performance matters!

GO!next is one of the products that we are developing within team Brokerage. It is the next generation web app interface of the Engel & Völkers brokerage core application that real estate agents use. The web app is since months in a pilot phase, where agents mainly in Spain, France, Italy and Germany are testing it to accomplish their daily business.

We use AngularJS framework, a MVW (Model View Whatever) frontend framework, to build GO!next.

As the functionalities kept growing, we faced, at some point, poor frontend performance causing serious slow-downs and even application crashes.


Hamburg - Chromecrash

In this post, I will go through two of the main reasons, memory leaks and AngularJS digest cycle, behind this and the solutions we adapted to improve our web app’s performance.


Memory leaks
Per design, the app uses overlays to guide the user through his daily business workflow.

Hamburg - GoNext!

Recording a runtime performance (using Chrome dev tools) while interacting with the user interface showed that JS heap size and the number of HTML nodes kept increasing even with forced garbage collections.

Hamburg - runtime Performance

As we are using a lot of custom directives and overlays, we had to make sure that every directive is cleaning after itself and that every closed overlay (its corresponding controller) cleans its $scope.


Here $scope.$destroy comes to help!!

Hamburg - Engel & Völkers Technology

But first of all, we have to distinguish between two kinds of "event listeners" in AngularJS: 

  • Scope event listeners registered via $on:
$scope.$on('event', function (event, data) {
...
});
  • Event handlers attached to elements via on or bind:
element.on('click', function (event) {
...
});

When $scope.$destroy() is executed, it will remove all listeners registered via $on on that $scope. It will not remove DOM elements or any attached event handlers.

So to deal with the second kind of listeners we have to call element.remove( ). When element.remove() is executed, that element and all of its children will be removed from the DOM as well as all attached event handlers (via for example element.on) but it will not destroy the $scope associated with the element.  

Combining  $scope.$destroy() and element.remove() ensure deletion of both listeners. Furthermore, any event handlers attached to elements outside the directive should be manually cleaned.


var windowClick = function () {
...
};
angular.element(window).on('click', windowClick);
scope.$on('$destroy', function () {
angular.element(window).off('click', windowClick);
});

The same goes for registered listeners on $rootScope:

var unregisterFn = $rootScope.$on('event', function () {});
$scope.$on('$destroy', unregisterFn);

This is needed, since $rootScope is never destroyed during the lifetime of the application.
And one more thing is to cancel $interval and $timeout as they both return promises:

var promise = $interval(function () {}, 1000);
scope.$on('$destroy', function () {
$interval.cancel(promise);
});

After the changes, recording a runtime performance for the same user interactions showed a better memory management:

Hamburg - Speicherlecks

AngularJS digest

AngularJS framework comes with a great feature, two-way data binding: meaning that a model can be updated from the controller or the view. This is achieved through watchers: each variable/expression bounded to the view is “watched” and when a digest cycle is triggered, AngularJS loops over all the watchers applying a dirty check and re-renders the view based on updated models.

Hamburg - AngularJS official documentation

As our app scales, binding counts increase and our $digest loop’s size increases. This hurts our performance when we have a large volume of bindings per application view. Unfortunately it was our case, ending up with thousands of watchers causing a longer digest cycle.



Hamburg - Watcher

To solve this problem, we used many techniques:


  • One-time data binding:From AngularJS docs “One-time expressions will stop recalculating once they are stable, which happens after the first digest if the expression result is a non-undefined value.”
    When we declare a value such as {{:: foo }} (instead of {{ foo }}) inside the DOM, once this value becomes defined, AngularJS will render it, unbind it from the watchers and thus reduce the volume of bindings inside the $digest loop.


  • Using ng-if instead of ng-show:
    Both directives hide/show HTML content on the view but the main difference is that ng-show will add CSS property display: hidden to the element (and thus its children) to hide it while ng-if will remove it from the DOM. Removing the element and all its children will deregister any watchers.


  • Filter in the controller when possible: We are using stateful filters especially for translation, which can not be optimized by AngularJS, so we made sure to move all possible filtering from the view to the controller.

  • Replacing endless scrolling with pagination: Using ng-repeat with large arrays had dramatic impact on our app’s performance, especially with the infinite scroll. We decided to replace it with a pagination in order to keep control over the number of watchers.
Hamburg - Screen Shot

*The result

Our journey in improving our core brokerage software performance is continuing on a daily basis. Next step will be optimizing pictures and thus page load time. We will keep you updated :)

Hamburg - Software Engineer Engel & Völkers Technology

Follow us on social media


Array
(
[EUNDV] => Array
(
[67d842e2b887a402186a2820b1713d693dd854a5_csrf_offer-form] => MTM5MjE5NzU3NkJ4d29xancwTDVhZWFIRzEycXAxcW9SdElHdVBqMTdV
[67d842e2b887a402186a2820b1713d693dd854a5_csrf_contact-form] => MTM5MjE5NzU3NnlHcUR0Y2VlTXVPUndLMHZkMW9zMnRmRlgxaUcwaFVG
)
)