Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Mysterious memory leaks in Angular JS Single Page Application

I am coming to you at great despair - been working on this for two days now.

We have a Single Page Application built with Angular JS. We use the $routeProvider in HTML5 mode to achieve the SPA routing. Functionality wise - all works great!

We have one global controller attached to the body element, there is one controller in the header for a quick search functionality and all other controllers are scoped to a route. There is some data shared between controllers like a currentUser object and a ViewRes object that contains string values for user's chosen language.

But, we noticed that the Chrome service takes too much RAM for our page. I used Chrome Profiles tool to see what is happening. I disabled most of our code that was using a complex directive and left out only the basics. Memory consumption was lowered a lot, but it is still obviously there. Any time I change a page, the memory increases.

In the Heap Snapshot it shows that most memory is taken by (closure) and (array). Also the Detached DOM tree is big. Please note that these snapshots are with bare elements of our application (header, footer and lightweight content). If I include our complex UI components, then memory jumps from 14MB to 50MB to 140MB... and more. Obviously we will take care for these directives, but I am concerned that our issue is global, not just to bad design of the directives.

All profiles

When I open the (array) element, I notice there are bunch of them with both shallow size and retained size of 6172. Tracking the scope of that object always leads to some ng directive, like ngShow, ngIf...

cache in function

As you can see from the image, the tree ends at 'cache in function()'. We use Angular 1.3.6.


EDIT: This project also includes jQuery. We were using jQuery 1.8.2 and when I switched to 1.11.2, switching between simple pages (simple ng-repeats and simple models) doesn't cause memory leaks anymore (no more detached DOM elements).

Now the complex directives are still giving me too much detached elements, so I am going to work on those now and I'll post the results here when I find out the cause.


like image 413
Gorgi Rankovski Avatar asked Jan 10 '23 00:01

Gorgi Rankovski


1 Answers

Difficult to say what your specific problems are, but common places for memory leaks in Angular include $interval, $watches and event handlers. Each of these functions creates a closure that is not cleaned up unless you explicitly delete it upon controller tear-down.

$interval in particular is nasty, as it will continue to run until you close the browser or the Web page - even if the user moves to a different tab or application, it won't stop running!

If you create references to DOM elements within these closures, you'll soon start chewing through memory, as the references are never released and the DOM tree becomes detached as the user moves from page to page.

To resolve this, ensure you handle the $destroy event in your controller (and your directives' controllers or link functions), and explicitly clean up after any intervals, watches or event handlers have been used.

You can do this by holding a reference to each $interval, watch or event handler and simply calling it as a function in the $destroy event handler.

For example:

// eventListener to remove
var eventListener = $scope.$on('eventName', function(){…});

// remove the eventListener when the $destroy event is fired
$scope.$on('$destroy', function(){
    // call the value returned from $scope.$on as a function to remove
    // the event listener
    eventListener();
}

// remove an event listener defined on a DOM node:
var elementEventListener = element.on('eventName', function(){…});

element.on('$destroy', function(){
    elementEventListener();
}

// Stop an interval
var stop = $interval(function(){...});
$scope.$on('$destroy', function(){
    stop();
}


// Finally, unbind a  $watch
var watchFn = $scope.$watch('someValue', function(newVal){…}

$scope.on('$destroy', function(){
     watchFn();
}

Finally, NEVER store DOM elements in the scope! (see Point #2 here for the reason why).

like image 60
Untangled Avatar answered Apr 28 '23 05:04

Untangled