即使调用 destroy(),Vue.js 也会发生 Turbolinks 内存泄漏 - 浏览器始终保留对 vue 实例应用程序的引用

san*_*e89 1 javascript turbolinks vue.js

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

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

在此输入图像描述

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

我们的应用程序使用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
    });
  }
}
Run Code Online (Sandbox Code Playgroud)

我在 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();
})
Run Code Online (Sandbox Code Playgroud)

更改屏幕一段时间后,我可以确认 Vue 对象仍然存在于内存中(WeakRef.deref()不会返回未定义),即使它们都被_isDestroyedVue 内部标记为内部。

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

在此输入图像描述

san*_*e89 5

天哪,我是来兜风的吗?我的应用程序中有 6 处内存泄漏。

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

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

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

在此输入图像描述

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

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

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

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

我们的应用程序中有 6 个以上的泄漏;下面我展示了其中之一,您会看到内存跟踪指向$notify,这是我们正在使用的一个库(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 应用程序的永久引用:

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

  • 如何确定内存流向的地方?您是如何确定 $notify 库到底泄漏了什么?您是否一一禁用组件并拍摄快照? (2认同)