Утечки памяти

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

В библиотеке Tippy.js утечки чаще всего связаны с жизненным циклом tooltip-компонентов, DOM-узлами и привязанными обработчиками событий.


Внутреннее устройство Tippy-инстансов

Каждый вызов tippy() создает экземпляр (instance), содержащий:

  • ссылку на целевой DOM-элемент
  • popper-объект (через Popper.js)
  • DOM-структуру tooltip
  • набор обработчиков событий
  • внутренние состояния и конфигурацию

Если экземпляр не уничтожен корректно, все эти ссылки остаются в памяти.


Основные причины утечек

1. Неуничтоженные экземпляры

При удалении элемента из DOM без вызова instance.destroy():

const instance = tippy(element);

// элемент удаляется из DOM
element.remove();

Экземпляр продолжает существовать, удерживая:

  • ссылку на удалённый DOM-элемент
  • обработчики событий
  • popper-инстанс

Решение:

instance.destroy();
element.remove();

2. Массовое создание tooltip без очистки

При динамическом рендеринге (например, в списках или таблицах):

items.forEach(item => {
  tippy(item);
});

При повторном рендере создаются новые инстансы без удаления старых.

Последствия:

  • дублирование tooltip
  • накопление обработчиков
  • рост памяти

Решение:

  • хранение ссылок на инстансы
  • уничтожение перед пересозданием
instances.forEach(i => i.destroy());
instances = items.map(item => tippy(item));

3. Использование делегирования без контроля

Tippy поддерживает делегирование:

tippy(parent, {
  target: '.child'
});

Если родитель удаляется без уничтожения делегированного инстанса, остаются:

  • обработчики на уровне родителя
  • ссылки на дочерние элементы

Решение:

instance.destroy();
parent.remove();

4. Замыкания и ссылки в кастомных функциях

Опции Tippy позволяют передавать функции:

tippy(element, {
  content: () => someLargeObject.data
});

Если someLargeObject большой и продолжает ссылаться на instance, возникает утечка.

Решение:

  • избегать хранения тяжелых объектов в замыканиях
  • очищать ссылки вручную

5. Неправильное использование interactive: true

Интерактивные tooltip создают дополнительные DOM-узлы и события:

tippy(element, {
  interactive: true
});

Если tooltip остаётся “живым” после удаления элемента:

  • hover/blur обработчики остаются активными
  • popper-элемент не удаляется

Методы управления жизненным циклом

instance.destroy()

Полностью удаляет:

  • tooltip DOM
  • popper
  • обработчики событий
  • внутренние ссылки
instance.destroy();

instance.hide() vs destroy()

Метод Освобождает память Удаляет DOM
hide() Нет Нет
destroy() Да Да

Глобальное уничтожение

import { hideAll } from 'tippy.js';

hideAll();

Не освобождает память, только скрывает tooltip.


Работа с фреймворками

React

Частая ошибка — создание tooltip внутри useEffect без очистки:

useEffect(() => {
  const instance = tippy(ref.current);
}, []);

Правильно:

useEffect(() => {
  const instance = tippy(ref.current);

  return () => {
    instance.destroy();
  };
}, []);

Vue

В директивах:

mounted(el) {
  el._tippy = tippy(el);
},
unmounted(el) {
  el._tippy.destroy();
}

Angular

Использование в ngOnDestroy:

ngOnDestroy() {
  this.tippyInstance.destroy();
}

Отладка утечек памяти

Chrome DevTools

  1. Вкладка Memory

  2. Heap Snapshot

  3. Поиск:

    • Detached DOM trees
    • объекты Tippy

Признаки утечки

  • рост числа Detached HTMLDivElement
  • увеличение числа event listeners
  • наличие Tippy объектов без DOM

Оптимизация при большом количестве tooltip

1. Использование делегирования

tippy(document.body, {
  target: '[data-tippy]'
});

Снижает количество инстансов.


2. Ленивое создание

tippy(element, {
  onShow(instance) {
    if (!instance._created) {
      instance.setContent(createContent());
      instance._created = true;
    }
  }
});

3. Переиспользование инстансов

Вместо удаления/создания:

instance.setProps(newProps);

Взаимодействие с Popper.js

Каждый Tippy-инстанс использует Popper:

  • вычисление позиции
  • подписки на scroll/resize

Если Popper не уничтожен:

  • продолжаются вычисления
  • остаются слушатели событий

destroy() автоматически вызывает очистку Popper.


Частые антипаттерны

Повторный вызов tippy() на одном элементе

tippy(element);
tippy(element);

Создаются два независимых инстанса.


Хранение instance в глобальной области

window.instance = tippy(element);

Сборщик мусора не может освободить память.


Отсутствие очистки при SPA-навигации

При смене страниц:

  • старые tooltip остаются
  • DOM очищен, но ссылки живы

Практическая стратегия предотвращения утечек

  • всегда вызывать destroy() перед удалением DOM
  • хранить ссылки на все инстансы
  • использовать lifecycle-хуки фреймворков
  • избегать лишних замыканий
  • минимизировать количество инстансов через делегирование
  • проверять через DevTools

Влияние утечек на производительность

  • рост потребления памяти
  • увеличение времени GC (garbage collection)
  • лаги при скролле
  • задержки при наведении
  • деградация UX при длительной работе приложения

Архитектурный подход

При разработке крупных приложений:

  • централизованный менеджер tooltip
  • единая точка создания/уничтожения
  • слабые ссылки (WeakMap) для хранения инстансов
const map = new WeakMap();

function attach(el) {
  const instance = tippy(el);
  map.set(el, instance);
}

function detach(el) {
  const instance = map.get(el);
  if (instance) instance.destroy();
}

Контроль памяти в динамических интерфейсах

Особое внимание требуется:

  • виртуализированные списки
  • infinite scroll
  • drag-and-drop интерфейсы
  • часто обновляемые таблицы

В таких случаях важно:

  • уничтожать tooltip при уходе элемента из viewport
  • использовать IntersectionObserver
  • минимизировать создание новых экземпляров

Связь с GC (Garbage Collector)

JavaScript освобождает память только если:

  • нет ссылок на объект

Tippy-инстанс удерживает:

  • DOM-элемент
  • обработчики
  • popper

Если хотя бы одна ссылка остаётся — объект не удаляется.


Критические зоны риска

  • модальные окна
  • всплывающие панели
  • временные элементы интерфейса
  • компоненты с коротким жизненным циклом

Рекомендации по коду

  • не вызывать tippy() без необходимости
  • не использовать глобальные переменные для хранения instance
  • всегда учитывать жизненный цикл UI
  • проверять, не создаётся ли tooltip повторно
  • регулярно профилировать приложение

Минимальный безопасный шаблон

function createTooltip(el) {
  const instance = tippy(el);

  return () => {
    instance.destroy();
  };
}

Такой подход гарантирует контроль над очисткой ресурсов.