Recently, we started development on the new version of our Online Video Platform. We decided to build OVP6 using Javascript, and specifically the AngularJS framework. Taking advantage of the strengths of AngularJS, we were quickly able to build the core functionalities of OVP6. Within months we were ready to present our beta-testing customers with a first version of our new platform. But… with real people using an application, real problems start coming to the surface. One of the most difficult ones, was the matter of performance.

Some of our customers asked us how we tackled these issues. In this technical blog, I will try to give some insights on how to debug memory issues in an AngularJS application.

An introduction to memory usage

When a computer program fails to return memory it no longer needs to the system, we use the term “memory leak”. Like many programming languages, Javascript uses a so-called garbage collector. This collector automatically releases objects from memory, if they are no longer referenced by any other in-memory objects. This implies however, that as long as there are references to an object, this object will not be cleaned up by the garbage collector.

garbage collector

In “old fashioned” web applications, where the browser would do a complete refresh of the page, every time a link is clicked, memory usage is not much of an issue. Requesting a new web page always forces the browser to clear all memory associated with the previously requested page. However, more and more web applications (including our new OVP6) are of the “single page” kind: a page is loaded once, and is updated dynamically asynchronously by retrieving data from a backend or local storage. Our challenge is finding the references between objects preventing them from being cleared from memory.

Using the Chrome Development Tools

We started investigating our memory leaks using the Chrome Developers Tools, available in Chrome using the View > Developer menu. Before we can solve any issues we might have, we first have to gain some insights in the memory usage of our application. The timeline tab is helpful for this.

  1. We check the “memory” box and start recording using the record icon on the left.
  2. We click around a little bit in our application, going from one screen to the other.
  3. We stop recording.

Chrome collects all recorded events and presents us the results.

Chrome Development Tools

What do we see here? The blue line represents the amount of memory used by Javascript while running our application. In the see-saw pattern we see the garbage collector clearing unused objects from memory. We visited 3 pages in our application, these pageviews are clearly separated by the longer flat blue lines indicating inactivity. The second spike is what we would like to see: memory is allocated (blue lines goes up) when we visit the page, and when we leave, it goes back to its old memory usage. All memory used by the page and its content seems to have been cleared. The bigger first and third spikes, however, tell a different story. Memory allocated by these pages, is not being cleared when we leave that page, slowly causing the total amount of memory used to increase. We see the same pattern in the yellow line, indicating the amount of Javascript listeners, and the green line, indicating the amount of DOM nodes which are currently present in memory.

We would like to dive deeper in one of those big spikes. We now use the “Profiles” tab of the Chrome Developers Tools. On this page we can take so called heap snapshot. A heap snapshot contains information about all objects in memory at a certain point in time. We create a snapshot on the page before the first big spike, a snapshot on a page after the first big spike, and a third snapshot on the page of the second (small) spike. For each of the snapshots, we can see a list of all in-memory objects. What we are interested in, is what objects are created between snapshot 1 and 2, and still present in snapshot 3. Luckily, the profiles tab give us an option just for that. This leaves us with a list of all objects who are currently in memory, but should not be there.

list of in-memory objects

Objects can be sorted by shallow size (the amount of memory these objects use themselves) or the retained size (the total amount of memory used by this object, and all objects it references). This makes it easier to search for the biggest problems. We can now expand the objects retaining the most memory, following the so called retaining path, to the objects which actually causes the object to stay in memory. However, we are dealing with an Angular application here. Angular helps us in our development by doing a lot of “magic” behind the scenes. It does this however by adding objects and functions, which purposes might not immediately be clear to an average Javascript developer. This makes it almost impossible to iterate though all the in-memory objects, and understand what should not be there. A different strategy is necessary.

The Profiles pane of the Developers tool has the ability to filter all objects on a certain class name. So if we want to find which of the objects from “spike 1” are still in memory in “spike 3” why not try using some easily identifiable class names there? For instance, let’s tag the controllers and directives used during the first spike, with something like this:

angular.module('myApp')
.controller('FooController', ['$scope', /* ... */,function($scope, /* ... */) {
    var FooControllerDebugTag = function() { };
    $scope.debugTag = new FooControllerDebugTag();
 
    /* .... */
});

We repeat the previous steps of creating heap snapshots, and try to search for the debug classes we’ve used. Behold! we find one of our debug tags. By expanding it’s context property, we can identify which scope object is responsible for keeping this tag in memory. So now we now exactly which of our components is responsible for a memory leak.

Fixing our application

With the knowledge from the previous paragraphs, it was much easier for us to find and fix various problems. Here are some examples of issues we’ve found using this method.

jQuery event handler cleanup

When attaching a jQuery event handler to a DOM element, you do this outside of Angular’s sphere of influence. When a scope is destroyed, Angular tries to clean up everything associated with the scope, but Angular doesn’t know anything about your jQuery listener. All objects referenced by the handler will stay in memory forever, unless you clean up the listener yourself when the scope is destroyed.

$document.on('keydown', function() {
$scope.doSomething();
});

$scope.$on("$destroy", function() {
$document.off('keydown');
});

Unregister rootScope listeners

It’s not uncommon to attach event handlers to the $rootScope. You should know however that since the $rootScope is never destroyed during the lifetime of your application, all objects referenced by the handler will never be cleared by the garbage collector. So either try to attach the listener to the scope of the component you are creating, or manually unregister the event handler when you no longer need it.

var remover = $rootScope.$on(‘$locationChangeSuccess’,function() {

$scope.doSomething();
});

$scope.$on("$destroy", function() {
remover();
});

Element cleanup

When you create DOM elements which are never attached to a scope, they will not be cleaned automatically by Angular when a scope is deleted. This results in “detached” DOM nodes, nodes which are in memory, but not in the DOM. If an element holds a reference to Javascript object, for instance the scope that was used to create it, these objects will stay in memory and not be cleanup by the Javascript garbage collector. The solution is to call .remove() on the element when you are done with it.

Conclusion

While not completely finished fixing all of our memory issues, we are confident that any future problems can be investigated using the tools mentioned in this blog post. Should you encounter any memory issues while creating a single page Javascript application, we hope this helps you identifying the issues and fixing them.