Memory leaks и их выявление

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

В Slim.js утечки памяти чаще всего появляются из-за неудалённых подписчиков событий, связей между компонентами, а также циклических ссылок, которые сборщик мусора не всегда корректно обрабатывает.


Основные источники утечек в Slim.js

  1. Подписчики событий (EventListeners) Slim.js активно использует события для связывания данных и компонентов. Если событие привязано к элементу, который был удалён из DOM, но слушатель остался, память под этот объект не освобождается.

    class MyComponent extends Slim {
      constructor() {
        super();
        this.handleClick = this.handleClick.bind(this);
      }
    
      handleClick() {
        console.log('Клик');
      }
    
      render() {
        return `<button id="btn">Click</button>`;
      }
    
      onConnected() {
        this.querySelector('#btn').addEventListener('click', this.handleClick);
      }
    
      onDisconnected() {
        this.querySelector('#btn').removeEventListener('click', this.handleClick);
      }
    }
    
    Slim.define('my-component', MyComponent);

    Ключевой момент: метод onDisconnected обязательно должен удалять слушатели, иначе кнопка и объект MyComponent останутся в памяти.

  2. Ссылки между компонентами Когда один компонент хранит прямую ссылку на другой, особенно через свойства или методы, они могут оставаться в памяти даже после удаления из DOM.

    class Parent extends Slim {
      childrenRefs = [];
    
      addChild() {
        const child = document.createElement('my-child');
        this.childrenRefs.push(child);
        this.appendChild(child);
      }
    
      removeChild() {
        const child = this.childrenRefs.pop();
        this.removeChild(child);
      }
    }

    Здесь важно очищать массив childrenRefs после удаления дочернего элемента, иначе объект останется доступным через ссылку.

  3. Замыкания и таймеры Использование setTimeout или setInterval внутри компонентов может вызвать утечки, если таймеры не очищаются при удалении компонента.

    class TimerComponent extends Slim {
      onConnected() {
        this.interval = setInterval(() => console.log('tick'), 1000);
      }
    
      onDisconnected() {
        clearInterval(this.interval);
      }
    }

Методы выявления утечек памяти

  1. Chrome DevTools – Memory Tab Инструмент позволяет делать heap snapshots, которые фиксируют текущее состояние памяти.

    • Сделать снимок памяти до и после создания/удаления компонента.
    • Сравнить количество объектов и их типы.
    • Обратить внимание на объекты, которые должны были быть удалены, но всё ещё присутствуют.
  2. Allocation instrumentation on timeline Позволяет отслеживать, какие объекты выделяются в памяти со временем. Полезно для выявления накопления объектов при повторном создании и удалении компонентов.

  3. WeakMap и WeakRef Использование WeakMap для хранения ссылок на компоненты снижает риск утечек, так как эти структуры не препятствуют сборке мусора:

    const componentStore = new WeakMap();
    
    function registerComponent(component) {
      componentStore.set(component, { data: 'meta' });
    }

Практические рекомендации для предотвращения утечек в Slim.js

  • Всегда удалять слушатели событий в onDisconnected.
  • Чистить массивы ссылок на дочерние компоненты и объекты при их удалении.
  • Очищать таймеры и интервалы в методе onDisconnected.
  • Использовать WeakMap или WeakRef для хранения вспомогательных объектов.
  • Минимизировать создание глобальных переменных внутри компонентов.
  • Проверять циклические ссылки: компоненты не должны хранить ссылки друг на друга без необходимости.

Пример комплексной очистки

class ComplexComponent extends Slim {
  constructor() {
    super();
    this.handleClick = this.handleClick.bind(this);
    this.childrenRefs = [];
  }

  handleClick() {
    console.log('clicked');
  }

  onConnected() {
    this.querySelectorAll('button').forEach(btn => btn.addEventListener('click', this.handleClick));
    this.interval = setInterval(() => console.log('tick'), 1000);
  }

  onDisconnected() {
    this.querySelectorAll('button').forEach(btn => btn.removeEventListener('click', this.handleClick));
    clearInterval(this.interval);
    this.childrenRefs.length = 0; // очистка ссылок
  }
}

Slim.define('complex-component', ComplexComponent);

В этом примере все потенциальные источники утечек: слушатели событий, интервалы и массивы ссылок, корректно очищены.


Ключевые выводы

  • Утечки памяти часто незаметны, но постепенно снижают производительность.
  • В Slim.js основными виновниками являются слушатели событий, ссылки между компонентами и таймеры.
  • Инструменты DevTools позволяют точно выявлять объекты, оставшиеся в памяти.
  • Правильная организация методов onConnected и onDisconnected решает большинство проблем с памятью.