AngularJS & jQuery Plugins Memory Leaks
Sooner or later working on single page applications you start worrying about JavaScript memory leaks. We're using AngularJS in our project and while it does a lot of magic behind the scene you still need to make sure jQuery plugins are getting unloaded when you leave the page because that stuff is out of AngularJS scope.
tl;dr
Use AngularJS $destroy
event
to unload your jQuery plugins and DOM events:
.directive('dtPicker', function() {
return {
link: function(scope, elm) {
elm.datetimepicker();
scope.$on('$destroy', function() {
elm.data('DateTimePicker').destroy();
});
}
};
});
Checking memory leak
First you need to check if there's a memory leak or not. Here's a great video that helped me to get started with timeline and profiling in chrome DevTools. It also has a good explanation on how the JavaScript garbage collector works and why memory leaks happen.
Example
To illustrate the issue I added a simple test page with two directives. Both of them use bootstrap datetimepicker jQuery plugin as an example and one of them have memory leak while another one doesn't.
In short: when you suspect a memory leak on some of your pages you need to open that page in incognito mode, run memory timeline recording and load-unload the page few times. Here's a short video where I test first datetime picker directive:
On the timeline you can see that number of DOM nodes and event listeners is constantly increasing, that's a sign of memory leak:
This process might be very time consuming, but it is the most straightforward and simple way I found so far.
AngularJS $destroy event
To fix this memory leak we need to unload the jQuery plugin when the page is changed or scope is destroyed.
AngularJS has a handy $destroy
event and
most of the jQuery plugins provide some kind of destroy
method. All you need to do is call that method on $destroy
event:
.directive('dtPicker', function() {
return {
link: function(scope, elm) {
elm.datetimepicker();
scope.$on('$destroy', function() {
elm.data('DateTimePicker').destroy();
});
}
};
});
If we try same steps on the example page for the new directive now, you can see that memory is getting unloaded properly and number of DOM nodes and event listeners stay the same:
AngularJS also triggers jQuery $destroy
event on the DOM element:
elm.on('$destroy', function() {
elm.data('DateTimePicker').destroy();
});
You can make a clean up this way as well if you'd like, I haven't found any difference so far.