4

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.


2
  • Can you replicate this in a plunker? Commented Jan 28, 2015 at 21:27
  • @NewDev that will be really hard to do. See my edited question. Thanks! Commented Jan 29, 2015 at 13:29

1 Answer 1

9

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).

Sign up to request clarification or add additional context in comments.

2 Comments

Thanks for your reply. The reason I hold references to DOM and use some events and $timeout in my directives is because I need to get the width of the element after its being parsed into the browser and then decide whether to display additional elements (actually arrows to scroll). The width is dynamic because it scales on different screens. Do you have suggestion for this?
There's nothing wrong with holding references to the DOM in your directive - just don't assign the scope variable to a DOM element. And always make sure you handle the $destroy event as I said above, and clean up your $watches, event handlers and $intervals.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.