首页 > 解决方案 > Vue.js 内存泄漏与 Turbolinks 即使调用 destroy() - 浏览器始终保持对 vue 实例应用程序的引用

问题描述

我有一个使用 Turbolinks 的 Rails 应用程序。如果您不熟悉,turbolinks 使documentandwindow对象始终保持不变(页面从不刷新),它通过 AJAX 拦截所有链接点击并替换 body 标记。

当导航到和从安装了 Vue 应用程序的页面时,这是我们观察到的内存/节点模式(它增长到无穷大):

在此处输入图像描述

如您所见,在每次页面更改时,内存只会增加,而不会被回收。

我们的 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
    });
  }
}

我对 Vue 应用程序或其他任何地方都没有任何参考window(实例化代码只是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()不返回未定义),即使它们都_isDestroyed被 Vue 标记为内部。

如何调试为什么 Vue 应用程序实例没有被垃圾收集以及为什么浏览器保留对它们的引用?

在此处输入图像描述

标签: javascriptvue.jsturbolinks

解决方案


男孩,我是来兜风的。我的应用程序中有 6 个内存泄漏。

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

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

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

在此处输入图像描述

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

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

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

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

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

在此处输入图像描述

是的,您可能会从其他人的代码中泄漏,这很糟糕。查看那个库,我发现罪魁祸首是在created钩子上定义的两个事件处理程序,它们从未被释放;我在这里发出了拉取请求。

  1. 冲洗并重复。在我们的 6 个内存泄漏中,通过这种方法诊断时,大多数都可以轻松解决。我们有一些东西:
  • 库中的“错误” mitt(微小的事件发射器);eventEmitter.off()应该清除所有事件发射器,但它没有;在这里打开一个问题;

  • created我们在(using )中订阅了 Vuex 事件,this.$store.subscribe()但忘记在 beforeDestroy() 中取消订阅;该subscribe()函数返回一个取消订阅的函数;将其保存在实例本身(如this.unsubscribeVuex = this.$store.subscribe(...))并this.unscubribeVuex在您的beforeDestroy()钩子中调用

  • 请注意您是否没有留下window.myApp = new Vue()参考资料;没有任何东西window.被垃圾收集;window.myApp = null;beforeDestroy钩的好主意;

  • Google Chart 也导致了内存泄漏;我在下面粘贴快速修复,请注意差异中我们如何泄漏对我们的 vue 应用程序的永久引用:

在此处输入图像描述 在此处输入图像描述 在此处输入图像描述


推荐阅读