Vue.js 即使调用了 destroy(),Turbolinks 也会导致内存泄漏 - 浏览器始终保持对 vue 实例应用程序的引用
Vue.js memory leak with Turbolinks even with destroy() being called - browser always keeps a referente to vue instance app
我有 Rails 个使用 Turbolinks 的应用程序。如果你不熟悉,turbolinks 使得 document
和 window
对象始终保持相同(页面永远不会刷新),并且它拦截所有 link 点击 AJAX 并替换 body 标签。
在安装了 Vue 应用程序的页面之间导航时,这是我们观察到的 memory/nodes 模式(它增长到无穷大):
如您所见,在每次页面更改时,内存只会增加,而不会被回收。
我们的App使用https://github.com/jeffreyguenther/vue-turbolinks,基本上就是这样的代码:
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
});
}
}
我没有对 window
或 Vue 应用程序的任何其他地方的任何引用(实例化代码只是 new Vue({ options})
;没有全局变量,现代代码使用 const/let。
没有从应用内添加 document.addEventListener
;所有事件侦听器都由 v-on 添加,Vue 在销毁时自动将其删除。
为了进一步调试,我修补了上面的代码以添加 WeakRef
的存储,并在销毁 Vue 实例之前将其正确设置,如下所示:
document.addEventListener('turbolinks:visit', function teardown() {
window.weakReferences = window.weakReferences || {};
window.weakReferences[new Date()] = new window.WeakRef(this);
this.$destroy();
})
切换屏幕一段时间后,我可以确认 Vue 对象仍然存在于内存中(WeakRef.deref()
没有 return undefined),即使它们都被标记为 _isDestroyed
Vue 内部。
如何调试为什么 Vue 应用程序实例没有被垃圾回收以及为什么浏览器保留对它们的引用?
语法应该类似于
beforeMount() {
document.addEventListener('turbolinks:visit', this.tearDown());
},
methods: {
tearDown() {
....
// Have this method in case if you want to perform any actions for the eventListener and not for destroying purpose
}
},
beforeDestroy() {
....de-allocate variable memories
document.removeEventListener('turbolinks:visit', this.tearDown());
}
天啊,我是来兜风的吗?我的应用程序中有 6 次内存泄漏。
要发现它们,请执行以下操作:
- 打开您的 Chrome devtools 并聚焦内存选项卡(我建议在 icognito 选项卡中执行此操作,这样扩展不会混淆测量);刷新您的应用程序,然后单击小垃圾桶(这将强制垃圾收集 - GC);
开始注意“Select Javascript VM 实例”中的 MB 指示符(您可能只有一行)。您可以忽略那里的 up/down 箭头指示符,只关注第一个数字,这是您的选项卡现在使用的 MB 数。
- 开始使用您的应用;在你的例子中,导航到和离开有 Vue 应用程序的页面;请注意内存是否一直在上升,或者当您离开 Vue 应用程序时它是否会下降;
在您的情况下,每次在应用程序中导航 TO 和 FROM 都会增加 20 MB 的内存使用量,最终在 10+ 次左右后达到 200MB;单击垃圾收集图标仅减少了大约 10MB 的使用量,因此很明显我们导致了泄漏。
将页面置于此泄漏状态并单击 GC 垃圾桶图标后,select单选选项中的“堆快照”。它将收集快照。点击它。
在我们的例子中,由于泄漏,我们的 Vue 应用程序一直保存在内存中。所以我们关注列表中的“VueComponent”项。单击小三角形并观察屏幕的下半部分。它将开始向您展示哪些代码片段将这些引用保留在内存中。有时你会很幸运,甚至在那里得到一些行号,你可以点击它,它会在源选项卡中打开。
我们在您的应用程序中有 6 次以上的泄漏;下面我展示了其中一个,你会看到内存跟踪指向 $notify
,这是我们使用的库(vue-notification):
是的,您可能会从其他人的代码中泄露信息,这真是太可惜了。查看该库,我发现罪魁祸首是 created
挂钩上定义的两个事件处理程序,它们从未被释放;我发出了拉取请求 here.
- 冲洗并重复。在我们的 6 次内存泄漏中,通过这种方法诊断时,大多数都很容易解决。我们有一些东西:
mitt
库中的 'bug'(微型事件发射器); eventEmitter.off()
应该清除所有事件发射器,但它没有;打开一个问题 here;
我们在 created
订阅了 Vuex 事件(使用 this.$store.subscribe()
),忘记在 beforeDestroy() 中取消订阅; subscribe()
函数 returns 取消订阅的函数;将其保存在实例本身(如 this.unsubscribeVuex = this.$store.subscribe(...)
)并在您的 beforeDestroy()
钩子
中调用 this.unscubribeVuex
注意你是否没有留下 window.myApp = new Vue()
参考资料; window.
中没有任何垃圾被收集;在 beforeDestroy
挂钩中设置 window.myApp = null;
是个好主意;
Google Chart 也导致内存泄漏;我正在粘贴下面的快速修复,请注意差异中我们如何泄漏对我们的 vue 应用程序的永久引用:
我有 Rails 个使用 Turbolinks 的应用程序。如果你不熟悉,turbolinks 使得 document
和 window
对象始终保持相同(页面永远不会刷新),并且它拦截所有 link 点击 AJAX 并替换 body 标签。
在安装了 Vue 应用程序的页面之间导航时,这是我们观察到的 memory/nodes 模式(它增长到无穷大):
如您所见,在每次页面更改时,内存只会增加,而不会被回收。
我们的App使用https://github.com/jeffreyguenther/vue-turbolinks,基本上就是这样的代码:
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
});
}
}
我没有对 window
或 Vue 应用程序的任何其他地方的任何引用(实例化代码只是 new Vue({ options})
;没有全局变量,现代代码使用 const/let。
没有从应用内添加 document.addEventListener
;所有事件侦听器都由 v-on 添加,Vue 在销毁时自动将其删除。
为了进一步调试,我修补了上面的代码以添加 WeakRef
的存储,并在销毁 Vue 实例之前将其正确设置,如下所示:
document.addEventListener('turbolinks:visit', function teardown() {
window.weakReferences = window.weakReferences || {};
window.weakReferences[new Date()] = new window.WeakRef(this);
this.$destroy();
})
切换屏幕一段时间后,我可以确认 Vue 对象仍然存在于内存中(WeakRef.deref()
没有 return undefined),即使它们都被标记为 _isDestroyed
Vue 内部。
如何调试为什么 Vue 应用程序实例没有被垃圾回收以及为什么浏览器保留对它们的引用?
语法应该类似于
beforeMount() {
document.addEventListener('turbolinks:visit', this.tearDown());
},
methods: {
tearDown() {
....
// Have this method in case if you want to perform any actions for the eventListener and not for destroying purpose
}
},
beforeDestroy() {
....de-allocate variable memories
document.removeEventListener('turbolinks:visit', this.tearDown());
}
天啊,我是来兜风的吗?我的应用程序中有 6 次内存泄漏。
要发现它们,请执行以下操作:
- 打开您的 Chrome devtools 并聚焦内存选项卡(我建议在 icognito 选项卡中执行此操作,这样扩展不会混淆测量);刷新您的应用程序,然后单击小垃圾桶(这将强制垃圾收集 - GC);
开始注意“Select Javascript VM 实例”中的 MB 指示符(您可能只有一行)。您可以忽略那里的 up/down 箭头指示符,只关注第一个数字,这是您的选项卡现在使用的 MB 数。
- 开始使用您的应用;在你的例子中,导航到和离开有 Vue 应用程序的页面;请注意内存是否一直在上升,或者当您离开 Vue 应用程序时它是否会下降;
在您的情况下,每次在应用程序中导航 TO 和 FROM 都会增加 20 MB 的内存使用量,最终在 10+ 次左右后达到 200MB;单击垃圾收集图标仅减少了大约 10MB 的使用量,因此很明显我们导致了泄漏。
将页面置于此泄漏状态并单击 GC 垃圾桶图标后,select单选选项中的“堆快照”。它将收集快照。点击它。
在我们的例子中,由于泄漏,我们的 Vue 应用程序一直保存在内存中。所以我们关注列表中的“VueComponent”项。单击小三角形并观察屏幕的下半部分。它将开始向您展示哪些代码片段将这些引用保留在内存中。有时你会很幸运,甚至在那里得到一些行号,你可以点击它,它会在源选项卡中打开。
我们在您的应用程序中有 6 次以上的泄漏;下面我展示了其中一个,你会看到内存跟踪指向 $notify
,这是我们使用的库(vue-notification):
是的,您可能会从其他人的代码中泄露信息,这真是太可惜了。查看该库,我发现罪魁祸首是 created
挂钩上定义的两个事件处理程序,它们从未被释放;我发出了拉取请求 here.
- 冲洗并重复。在我们的 6 次内存泄漏中,通过这种方法诊断时,大多数都很容易解决。我们有一些东西:
mitt
库中的 'bug'(微型事件发射器);eventEmitter.off()
应该清除所有事件发射器,但它没有;打开一个问题 here;我们在
中调用created
订阅了 Vuex 事件(使用this.$store.subscribe()
),忘记在 beforeDestroy() 中取消订阅;subscribe()
函数 returns 取消订阅的函数;将其保存在实例本身(如this.unsubscribeVuex = this.$store.subscribe(...)
)并在您的beforeDestroy()
钩子this.unscubribeVuex
注意你是否没有留下
window.myApp = new Vue()
参考资料;window.
中没有任何垃圾被收集;在beforeDestroy
挂钩中设置window.myApp = null;
是个好主意;Google Chart 也导致内存泄漏;我正在粘贴下面的快速修复,请注意差异中我们如何泄漏对我们的 vue 应用程序的永久引用: