This guide shows how to create an AngularJS application that consumes more and more memory until, eventually, the browser process crashes on your users.
Although this guide has been written for Angular 1 originally, most of the advice is relevant for all client-side JavaScript code.
To inspect the amount of memory consumed by your Javascripts in Chrome:
Shift + ESC
to see a list of Chrome processesNote that you can manually run the Javascript garbage collector:
Ctrl+Shift+J
)Note that if you created a proper memory leak, the garbage collector won't be able to reclaim much memory. That's why it's a leak.
You can also take a look at the contents of your memory:
Ctrl+Shift+J
)If your directives register event listeners to global elements like window
, document
or <body>
, these listeners continue to exist after your directive element is destroyed. This also prevents referencing closures, scopes, etc. from being garbage-collected.
This code leaks memory:
@app.directive 'foo', ->
link: (scope, element, attributes) ->
$(document).on 'click', ->
# code here
To prevent leaks you must manually unregister listeners from all elements that are not your own descendants.
This code does not leak memory:
@app.directive 'foo', ->
link: (scope, element, attributes) ->
listener = ->
# code here
$(document).on 'click', listener
scope.$on '$destroy', ->
$(document).off 'click', listener
You probably have some directives to integrate classic jQuery plugins that are not AngularJS-aware: Modal dialogs, tooltips, maps, lightboxes, etc. If you fail to clean up those plugins after the directive's element is destroyed, you have no guarantee that it releases all of its resources.
The following code might leak memory:
@app.directive 'awesomeMap', ->
link: (scope, element, attributes) ->
AwesomeMap.init(element)
To prevent leaks, read the plugin's documentation for a way to destroy the plugin instance when you're done:
@app.directive 'awesomeMap', ->
link: (scope, element, attributes) ->
map = AwesomeMap.init(element)
scope.$on '$destroy', ->
map.nuke()
Note that the nuke()
method is just an example. Every plugin has its own way of disposing it, so check out the respective documentation.
If the plugin you're using doesn't have a way to dispose it's instances, it is probably not suited for a persistent Javascript environment and you should pick a plugin that is.
If you're using $rootScope
as a global event bus, listeners continue to exist after your directive element is destroyed. This also prevents referencing closures, scopes, etc. from being garbage-collected.
This code leaks memory:
@app.directive 'foo', ($rootScope) ->
link: (scope, element, attributes) ->
$rootScope.$on 'event', ->
scope.attribute = value
To allow the scope to be garbage collection, remove yourself as a $rootScope
listener when your scope
gets destroyed:
@app.directive 'foo', ($rootScope) ->
link: (scope, element, attributes) ->
unsubscribe = $rootScope.$on 'event', ->
scope.key = value
scope.$on '$destroy', unsubscribe
When your directive register timers using setTimeout
, setInterval
or $timeout
, they will not be cleaned up when the element is destroyed.
E. g. this <clock>
directive leaks memory every time it is destroyed and re-created:
@app.directive 'clock', ->
link: (scope, element, attributes) ->
updateTime = ->
var now = new Date();
element.text(now.toString())
setInterval updateTime, 1000
The version below clears the interval and doesn't leak memory:
@app.directive 'clock', ->
link: (scope, element, attributes) ->
updateTime = ->
var now = new Date();
element.text(now.toString())
interval = setInterval(updateTime, 1000)
scope.$on '$destroy', ->
clearInterval(interval)
A re-arming timer is a timeout that, when triggered, schedules itself again and again.
Because $timeout
returns a promise for the timer's execution, and because Coffeescript uses the last expression in a block as implicit return value, this pattern is a great way to create an infinitely growing object graph.
More details here
Show archive.org snapshot
.
This code leaks more and more memory over time:
@app.directive 'foo', ($timeout) ->
link: (scope, element, attributes) ->
tick = ->
# some code here
$timeout tick, 1000
tick()
To prevent the leak, explicitly return
undefined at the end of tick()
:
@app.directive 'foo', ($timeout) ->
link: (scope, element, attributes) ->
tick = ->
# some code here
$timeout tick, 1000
return
tick()
Note that the "good" example above is still leaking memory since it doesn't stop the re-arming timer when scope
is destroyed.
See above.
A great way to create memory leaks is to output a structured objects (e. g. an Angular scope) to your browser console:
console.log(scope)
Since the browser console allows you to dive into scope
and traverse its object graph, scope
can now no longer be garbage collected. So in trying to debug a memory leak using console.log
you're making everything worse.
You can use
$new
Show archive.org snapshot
create new scopes from an existing scopes:
myScope = scope.$new()
You are now responsible for cleaning up myScope
when you're done with it, by calling myScope.$destroy()
. Javascript's garbage collection will not do this for you.
If you fail to clean up myScope
it will forever be linked to scope
and continue to participate in model change detection and listener notification.
$rootScope
Are you abusing $rootScope
to watch arbitrary code expressions outside of AngularJS components? Like this:
observer = ->
# observe expression
$rootScope.$watch observer, (new, old) ->
# code to run if the expression changes
This watcher will continue to be called indefinitely unless you clean up after yourself:
observer = ->
# observe expression
unsubscribe = $rootScope.$watch observer, (new, old) ->
# code to run if the expression changes
# later, when you no longer need the watch
unsubscribe()
This is more about programming 101 but AngularJS but nonetheless:
If you create a caching mechanism for performance, but fail to contain the cache size, it will consume more and more memory and prevent the garbage collector from reclaiming memory.
This userRepository
service caches users once they were loaded from a remote server and, hence, leaks memory:
app.factory 'userRepository', ($q) ->
userCache = {}
findUser: (email) ->
if cachedUser = userCache[email]
$q.when(cachedUser)
else
loadUserFromServer(email).then (user) ->
userCache[email] = user
You can score extra leakage for caching DOM nodes or AngularJS scopes in this fashion.
To prevent leaks, make sure the cache has a limited size, and automatically prunes itself when new entries are inserted (FIFO or least-recently-used).
Avoid caching highly linked objects like DOM nodes, AngularJS scopes or domain models that hold a lot of references to other models.
This really affects all jQuery 1.x code: jQuery doesn't store information about event listeners and data
values with the element itself. This information is instead stored in the global $.cache
object. Every time you add an event listener or data value to a jQuery object, $.cache
gains another entry.
The only way that a $.cache
entry gets deleted is when you call remove()
on the element that put it there!
We have our own card about this issue: How to create memory leaks with jQuery 1.x.
Note that Angular (sometimes?) adds an element's associated scope as data('$scope')
. This means that not only can't the element be garbage-collected, its scope (and parent scopes and child scopes) can't be collected either.
The AngularJS team is diligently fixing memory leaks, unfortunately these issues are very hard to reproduce and debug.
Every AngularJS version has its own list of leak-releated issues:
In general, more recent versions appear to have more safeguards in place.
The Batarang Chrome extension has
its own memory leaks
Show archive.org snapshot
.
Also, since it displays contents of scopes and such, it might prevent these scopes from being garbage collected.
The same might be true for any other browser extension or plugin.
When hunting for memory leaks, use an icognito window or disable all extensions.
Some people
are seeing leaks when using ng-animate
Show archive.org snapshot
.
Some users of the angular-ui router are getting leaks on every state change Show archive.org snapshot .
Iterating over non-primitives using ng-repeat
seems to create objects that cannot be garbage-collected
Show archive.org snapshot
.
A fix for this might be part of the 1.3.15 release of AngularJS.