I have a Rails apps that uses Turbolinks. If you're not familiar, turbolinks makes so that the document
and window
object always remains the same (the page is never refreshed), and it intercepts all link clicks via AJAX and replaces the body tag.
When navigating TO and FROM a page that has a Vue app mounted, this is the memory/nodes pattern that we observe (it grows to infinity):
As you can see, on each page change, memory only increases, never being reclaimed.
Our App uses https://github.com/jeffreyguenther/vue-turbolinks, which is basically this code:
beforeMount: function() {
if (this === this.$root && this.$el) {
document.addEventListener('turbolinks:visit', function teardown() {
this.$destroy();
document.removeEventListener('turbolinks:visit', teardown);
})
// cache original element
this.$cachedHTML = this.$el.outerHTML;
// register root hook to restore original element on destroy
this.$once('hook:destroyed', function() {
if( this.$el.parentNode )
this.$el.outerHTML = this.$cachedHTML
});
}
}
I don't have any reference on window
or anywhere else to the Vue app (the instantiation code is just new Vue({ options})
; no global variables, modern code using const/let.
There are no document.addEventListener
being added from within the app; all event listeners are being added by v-on, which are removed by Vue automatically on destroy.
Trying to debug even further, I patched the code above to add a storage of WeakRef
and I set it right before destroying the Vue instance, like this:
document.addEventListener('turbolinks:visit', function teardown() {
window.weakReferences = window.weakReferences || {};
window.weakReferences[new Date()] = new window.WeakRef(this);
this.$destroy();
})
After changing screens for a while, I can confirm the Vue objects are still present in memory (WeakRef.deref()
doesnt return undefined), even tough they are all marked as _isDestroyed
internally by Vue.
How can one go debugging why the Vue app instances are not being garbage collected and why the browser is keeping a reference to them?
Boy, was I in for a ride. There were 6 memory leaks in my app.
To discover them, do this:
Start noting the indicator of MB in the "Select Javascript VM Instance" (you'll probably have one line there only). You can ignore the up/down arrow indicators there, focus only on the first number, which is how many MB your tab is using right now.
In your case, navigating TO and FROM the app increased memory usage of 20 MB every time, eventually reaching 200MB after 10+ times or so; clicking the Garbage Collect icon only decreased usage of around 10MB, so it was obvious we head a leak.
After you have put the page in this leak state and clicked the GC trash icon, select "Heap snapshot" in the radio options. It will collect a snapshot. Click on it.
In our case, our Vue app was being kept in memory due to the leak. So we focused on the "VueComponent" items in the list. Click the little triangles and watch the lower part of the screen. It will start showing you what pieces of code are retaining those references in memory. Sometimes you'll get lucky and even get some line numbers there, that you can click and it will open in the sources tab.
We had 6+ leaks in your app; below I'm showing one of them, and you'll see the memory trace is pointing to $notify
, which was a lib (vue-notification) we were using:
Yes, you can have leaks from other people's code, that's a bummer. Looking at that lib, I could find the culprit was two event handlers defined on the created
hook, that were never released; I issued a pull request here.
A 'bug' in the mitt
library (tiny event emitter); eventEmitter.off()
should clear all event emitters, but it didn't; opened an issue here;
We subscribed to Vuex events in created
(using this.$store.subscribe()
) and forgot to unsubscribe in the beforeDestroy(); the subscribe()
function returns a function to unsubscribe; save it on the instance itself (like this.unsubscribeVuex = this.$store.subscribe(...)
) and invoke this.unscubribeVuex
in your beforeDestroy()
hoook
Watch out if you are not leaving window.myApp = new Vue()
references around; nothing in window.
gets garbage collected; good idea to set window.myApp = null;
in your beforeDestroy
hook;
Google Chart was also causing a memory leak; I'm pasting the quick fix below, notice in the diffs how we were leaking permanent references to our vue application: