Устранение утечек памяти

Введение

Если вы разрабатываете приложения на Vue, тогда вам нужно следить за утечками памяти. Эта проблема особенно важна в одностраничных приложениях (SPA), потому что идея использования SPA состоит в отсутствии пользователям необходимости обновлять страницы браузера, поэтому задачей JavaScript приложения также будет и очистка в компонентах от лишнего.

Утечки памяти в приложениях Vue обычно не исходят от самого Vue, скорее они могут происходить при интеграции других библиотек в приложение.

Простой пример

В этом примере показана утечка памяти, вызванная использованием библиотеки Choices.js внутри компонента Vue без очистки ресурсов должным образом. Далее мы покажем как удалять остающееся после Choices.js и избежать утечки памяти.

В примере ниже, мы загружаем в select большое число вариантов выбора, а также используем кнопку для отображения/скрытия с помощью директивы v-if, чтобы добавлять и удалять список из виртуального DOM. Проблема в этом примере заключается в том, что директива v-if удаляет родительский элемент из DOM, но не выполняет дополнительную очистку DOM от частей, созданных Choices.js, что и вызывает утечку памяти.

<link rel="stylesheet prefetch" href="https://joshuajohnson.co.uk/Choices/assets/styles/css/choices.min.css?version=3.0.3">
<script src="https://joshuajohnson.co.uk/Choices/assets/scripts/dist/choices.min.js?version=3.0.3"></script>

<div id="app">
  <button
    v-if="showChoices"
    @click="hide"
  >Скрыть</button>
  <button
    v-if="!showChoices"
    @click="show"
  >Показать</button>
  <div v-if="showChoices">
    <select id="choices-single-default"></select>
  </div>
</div>
new Vue({
  el: "#app",
  data: function () {
    return {
      showChoices: true
    }
  },
  mounted: function () {
    this.initializeChoices()
  },
  methods: {
    initializeChoices: function () {
      let list = []
      // загружаем в наш select множество вариантов,
      // что вызовет большое использование памяти
      for (let i = 0; i < 1000; i++) {
        list.push({
          label: "Item " + i,
          value: i
        })
      }
      new Choices("#choices-single-default", {
        searchEnabled: true,
        removeItemButton: true,
        choices: list
      })
    },
    show: function () {
      this.showChoices = true
      this.$nextTick(() => {
        this.initializeChoices()
      })
    },
    hide: function () {
      this.showChoices = false
    }
  }
})

Чтобы увидеть эту утечку памяти в действии, откройте этот пример на CodePen с помощью Chrome и затем откройте Диспетчер задач Chrome. Чтобы открыть на Mac, выберите в верхнем меню > Окно > Диспетчер задач или на Windows с помощью сочетания клавиш Shift+Esc. Теперь, нажимайте кнопку показать/скрыть около 50 раз. Вы сможете увидеть увеличение использованной памяти в Диспетчере задач Chrome, которая не будет освобождена.

Пример утечки памяти

Исправление утечки памяти

В примере выше, мы можем использовать наш метод hide() для выполнения очистки и устранения утечки памяти перед удалением select из DOM. Для этого мы будем хранить свойство в нашем экземпляре Vue и будем использовать API плагина Choices в методе destroy() для выполнения необходимых операций очистки.

Проверьте использование памяти в обновлённом примере на CodePen.

new Vue({
  el: "#app",
  data: function () {
    return {
      showChoices: true,
      choicesSelect: null
    }
  },
  mounted: function () {
    this.initializeChoices()
  },
  methods: {
    initializeChoices: function () {
      let list = []
      for (let i = 0; i < 1000; i++) {
        list.push({
          label: "Item " + i,
          value: i
        })
      }
      // Сохраняем ссылку на экземпляр плагина
      // в объекте data экземпляра Vue
      this.choicesSelect = new Choices("#choices-single-default", {
        searchEnabled: true,
        removeItemButton: true,
        choices: list
      })
    },
    show: function () {
      this.showChoices = true
      this.$nextTick(() => {
        this.initializeChoices()
      })
    },
    hide: function () {
      // теперь мы можем использовать ссылку на Choices
      // для выполнения необходимых плагину операций очистки
      // перед удалением элементов из DOM
      this.choicesSelect.destroy()
      this.showChoices = false
    }
  }
})

Подробнее о значимости

Управлением памятью и тестированием производительности часто легко можно пренебречь в спешке выпустить готовый продукт, но, тем не менее, сохранение небольшого количества используемой памяти по-прежнему важно для пользовательского опыта использования в целом.

Определите типы устройств, которые ваши пользователи могут использовать и какой сценарий работы с приложением может быть. Могут ли они использовать ноутбуки или мобильные устройства с небольшим количеством памяти? Будут ли ваши пользователи обычно совершать много переходов между страницами приложения? Если ответы на эти вопросы — «да», тогда хорошие практики управления памятью могут помочь избежать вам наихудшего сценария сбоя браузера пользователя. Даже если ни один ответ на вопрос не будет «да», вы по-прежнему можете ухудшить производительность вашего приложения при длительном использовании, если не будете осторожны.

Пример из жизни

В примере выше, мы использовали директиву v-if чтобы проиллюстрировать утечку памяти, но более распространённый сценарий из жизни возникает при использовании vue-router для маршрутизации по компонентам в одностраничном приложении (SPA).

Также, как и директива v-if, vue-router удаляет элементы из виртуального DOM и заменяет их новыми элементами при навигации пользователя по вашему приложению. Хук жизненного цикла beforeDestroy() — хорошее место для решения подобной проблемы в приложениях на основе vue-router.

Мы могли бы переместить нашу очистку в хук beforeDestroy() следующим образом:

beforeDestroy: function () {
  this.choicesSelect.destroy()
}

Альтернативы

Мы обсудили управление памятью при удалении элементов, но что, если вы намеренно хотите сохранять состояние и сохранить элементы в памяти? В этом случае вы можете использовать встроенный компонент keep-alive.

Когда вы оборачиваете компонент с помощью keep-alive, его состояние будет сохранено, и следовательно, останется в памяти.

<button @click="show = false">Скрыть</button>
<keep-alive>
  <!-- my-component будет сохраняться в памяти даже при удалении -->
  <my-component v-if="show"></my-component>
</keep-alive>

Эта техника может быть полезна для улучшения пользовательского опыта работы с приложением. Например, представьте, что пользователь начинает вводить комментарий в текстовое поле и затем решает перейти на другую страницу. Если пользователь вернётся обратно, то его комментарий останется сохранён в поле.

С тех пор, как начнёте использовать keep-alive, у вас появится доступ к двум дополнительным хукам жизненного цикла: activated и deactivated. Если вы хотите выполнить очистку или изменить данные при удалении компонента с keep-alive, вы можете сделать это в хуке deactivated.

deactivated: function () {
  // удаление любых данных, которые не требуется хранить
}

Подытожим

Vue позволяет легко разрабатывать потрясающие реактивные JavaScript-приложения, но вам всё равно нужно уделять внимание утечкам памяти. Эти утечки обычно происходят при использовании дополнительных сторонних библиотек, которые манипулируют DOM вне Vue. Не забудьте проверить ваше приложение на утечки памяти и предпринять соответствующие шаги для добавления необходимых очисток в компонентах, где это необходимо.