Mysterious memory leaks in Angular JS Single Page Application - angularjs

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

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

Related

How to save DOM manipulation changes?

Part of the code in my controller deletes a DOM element:
MetrofficeApp.controller('EmployeesCtrl', function($scope) {
...
angular.element(deleteElem).remove();
$scope.$apply();
When I navigate away from this page using angular, and then come back to the same page where the deleted element is - the element is visible again.
What must I do besides $scope.$apply() to make the changes permanent (save DOM changes) between navigating pages?
I feel like you have a fundamental misunderstanding about the DOM. Every time you navigate back to a page, all code is re-invoked and templates are recreated. So, it is correct behavior that the DOM is created again.
My guess is that you have some underlying model that is visualized by the DOM. Rather than deleting components of the DOM, you should be deleting the part of the model that is visualized by the DOM (and pushing that change to the server). This way, the next time you navigate to the DOM, the model is consistent and the deleted item is no longer shown.
And a smaller point, but still important: controllers should not be manipulating the DOM directly. You should be creating directives for that.
You need to set some flag on the scope object which the angular js template can use to determine whether the show the dom element in question.
E.g.
// controller code
scope.shouldShowElement = some.flag;
// angular template
<div ng-show="shouldShowElement">...</div>
Angular templates have access to all the variables on the scope - if, at a later point, you do:
// controller code
scope.shouldShowElement = true;
scope.$apply();
Your template will update to reflect that change

AngularJS: How to cache a rendered directive?

I have a directive (), it renders a large number (>10000) of items in a table. It is used in one controller's view.
Normally, when I first visit this controller, it will take a long time to render the directive.
When I leave this controller and visit it again, it re-renders again. Is there a way to cache the rendered directive so that it can show instantly in the second visit?
just an idea, take the generated html and store it either in a service or in the local/session storage then you can check its validity using a timestamp or a data version and just reload that existing html into the container element, this would fix dependencies issues and would help you to build a more independent directive since you don't have to rely on the dom position and also this way several directive instances can render the same dataset if you use attributes for configuring your directive.
or you could render DOM in an asynchronous manner using a recursion function with a $timeout call something like
function Draw(data,index){
if(idx==data.length)
return;
//draw element idx
$timeout(function(){
Draw(data,index+1);
})
}
this way the directive will take longer but results will appear faster in the screen and you don't take the risk of blocking your browser
Is the performance hit the actual DOM rendering or some data processing that's done in the directive? If DOM manipulation is the issue, you could try hiding the content with an ng-show/ng-hide when you navigate away from the controller rather than unloading it. A good way to do this (if you're using ui-router) would be to put the directive in a parent view and have the content hidden or shown depending on which child view is active.
If it's data processing that's the issue, you can move the logic to a service. Services aren't unloaded when you change views, so it will be able to process the data once and supply it to the controller whenever is is loaded without additional processing.

How to reduce/remove memory leaks in Angular application

I am optimizing my large Angular App. As I found a that Google DevTools is very good to detect problems. As I just have started learning about DevTools, I am very confused about memory leaks.
When I move back and from to different pages in my app, Profile heap Snapshot size is increasing again and again so I think there are some object which is not being cleaned by GC and that's why my app is getting slow after sometime so how to solve this. Please help.
Note
This is what I understand using DevTools, please correct me if I am wrong. Other suggestions are welcome.
Till now what I have used
AngularOnce directive for reducing watch whenever required.
QuickList directive to replace ng-repeat with quick-ng-repeat.
InView Directive, to handle large list so I am removing DOM which is not in viewport.
Lazy load approach from ngInfiniteScroll directive.
Remove bindings to avoid memory leaks, Use Scopes
$destroy() Method.
Note:
The most likely culprit of memory leak in Angular is JQuery used in
your directives. If you attach an event-listener in your directive
using a JQuery plugin, the latter would keep a reference to your DOM
even after Angular deletes its own reference to the DOM, which means
it would never be garbage-collected by the browser, which in turn
means “Detached DOM tree” in your memory
In your Directive keep practice for unbinding the jQuery Event.
$destory Method which can be used to clean up DOM bindings before an
element is removed from the DOM.
$scope.$on("$destroy",function() {
$( window ).off( "resize.Viewport" );
});
Don't Forget To Cancel $timeout Timers In Your $destroy Events In
AngularJS
$scope.$on("$destroy",function( event ) {
$timeout.cancel( timer );
});

AngularJS and $rootScope

I am trying to understand the lifetime of scopes in Angular and was wondering if doing $rootScope.$on or $rootScope.$watch ever goes out of scope (in the traditional sense)?
Does Angular have any type of Garbage Collection or anything like that?
Do you have to worry about releasing objects ($scopes)?
The entirety of angular scopes is a rather large piece of documentation, but you can start here or here.
The short, lofty answer is that angular inspects your modules (angular.module('myapp')) to create a complex tree of dependencies. Then the DOM is parsed to identify "ng" tags or other custom directives, which go through a compile phase (that constructs the custom HTML) and a linking phase (that generates a scope and attaches it to that element). That is about 1/100th of what really goes on, but it's a start.
Angular does perform garbage collection, but almost all of it happens behind the scenes. When a $scope is scheduled to be removed, one of the last things it does is fire a $scope.$destroy(), which you can hook into with $scope.$on('$destroy', function() { .. }) to perform any cleanup. Then again, in most instances you don't have to worry about cleanup or "releasing" anything, as angular takes care of that as well.
:Edit:
I also stumbled across this, which explains it best by far-- understanding scopes.

AngularJS performance: How to update only the scopes I know need to be updated?

I have an angular scope containing two things:
a giant table with 10k rows that takes a second to render
some small extra information on top of it in a fixed overlay header bar
Depending on how far you have scrolled down the page/table, I have to update one of the small info bits in the header; you can think of it as a % counter how far you've scrolled.
To get the current scroll position, I have written a directive (you can also find it here):
app.directive "setWindowScroll", ->
restrict: "E"
scope:
setVariable: "="
link: (scope) ->
$(window).scroll ->
scope.$apply ->
scope.setVariable = $(window).scrollTop()
My problem is that this makes scrolling around in the table u*nsuably slow*. This is because the veriable I write to with my directive is in the extra-info in the scope, and changing that seems to cause angular to dirty-check the whole table for changes when my $apply is called.
I know that this scrolling will never change anything in my table, and would like to restrict my $apply to affect the header part of my site.
How can I make angular not dirty check the table?
Angular does the dirty checking under a process called digest. When you do a call to $scope.$digest() it will propagate to every child scope of $scope. To notify angular about changes you usually do a $scope.$apply(). At the end of the $apply() angular does a digest on the root scope, which triggers a diggest on every scope in your application. So to avoid the digest on in your scope with the big table scope, you should make sure that it is not the child of the extra information scope, and rather than doing an $scope.$apply() in your directive you could do a $scope.$digest(). This might be a bit confusing so I've made a plunker that tries to show the difference. Open your console and see the difference in the scroll event and button click:
http://plnkr.co/edit/45gKhAIt0DrozS7f0Bz2?p=preview
// this will only cause a digest in the current scope and its children
angular.element($window).bind('scroll',function(){
$scope.scrollY = $window.scrollY;
$scope.$digest();
})
// this will cause a digest in every scope
angular.element($window).bind('scroll',function(){
$scope.scrollY = $window.scrollY;
$scope.$apply();
})
When all this is said, it's a very unusual thing to do - and probably not a good idea for a number of reasons (angular doesnt scale well with thousands of elements, you can't use any of the angular event directives (ngClick etc) because they're all wrapped in $apply) - but if you can't render the table on the server side you can give this a try.
I would like to know if this is possible to do too, but -- regardless -- you might want to consider not drawing the entire table all at once.
Instead limit it (and the amount of associated data/controllers in Angular-world) to just the rows that are visible based on current scroll position + some cached rows above and below. You can still keep all the data in an array on the client, but only expose a small subset to Angular at any time.
One way to do it might be to have a small array in Angular-world which you render using ng-repeat, and then you add and remove elements in this small angular array from the big non-angular array based on scroll position.
This way Angular only knows about a small subset of your data, and should render much faster. I think this should work quite well in general because it's also much less work for the browser which doesn't need to maintain a DOM tree and rendering for 10k rows.

Resources