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 使得 documentwindow 对象始终保持相同(页面永远不会刷新),并且它拦截所有 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 次内存泄漏。

要发现它们,请执行以下操作:

  1. 打开您的 Chrome devtools 并聚焦内存选项卡(我建议在 icognito 选项卡中执行此操作,这样扩展不会混淆测量);刷新您的应用程序,然后单击小垃圾桶(这将强制垃圾收集 - GC);

开始注意“Select Javascript VM 实例”中的 MB 指示符(您可能只有一行)。您可以忽略那里的 up/down 箭头指示符,只关注第一个数字,这是您的选项卡现在使用的 MB 数。

  1. 开始使用您的应用;在你的例子中,导航到和离开有 Vue 应用程序的页面;请注意内存是否一直在上升,或者当您离开 Vue 应用程序时它是否会下降;

在您的情况下,每次在应用程序中导航 TO 和 FROM 都会增加 20 MB 的内存使用量,最终在 10+ 次左右后达到 200MB;单击垃圾收集图标仅减少了大约 10MB 的使用量,因此很明显我们导致了泄漏。

将页面置于此泄漏状态并单击 GC 垃圾桶图标后,select单选选项中的“堆快照”。它将收集快照。点击它。

在我们的例子中,由于泄漏,我们的 Vue 应用程序一直保存在内存中。所以我们关注列表中的“VueComponent”项。单击小三角形并观察屏幕的下半部分。它将开始向您展示哪些代码片段将这些引用保留在内存中。有时你会很幸运,甚至在那里得到一些行号,你可以点击它,它会在源选项卡中打开。

我们在您的应用程序中有 6 次以上的泄漏;下面我展示了其中一个,你会看到内存跟踪指向 $notify,这是我们使用的库(vue-notification):

是的,您可能会从其他人的代码中泄露信息,这真是太可惜了。查看该库,我发现罪魁祸首是 created 挂钩上定义的两个事件处理程序,它们从未被释放;我发出了拉取请求 here.

  1. 冲洗并重复。在我们的 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 应用程序的永久引用: