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.
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.
Per design, the app uses overlays to guide the user through his daily business workflow.
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.
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!!
But first of all, we have to distinguish between two kinds of "event listeners" in AngularJS:
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.
The same goes for registered listeners on $rootScope:
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:
After the changes, recording a runtime performance for the same user interactions showed a better memory management:
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.
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.
To solve this problem, we used many techniques:
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 :)