Memory leaks

В контексте разработки с использованием Stencil важно понимать, как фреймворк управляет жизненным циклом компонентов и как это влияет на использование памяти. Несмотря на автоматическое управление рендерингом и обновлением виртуального DOM, некорректное обращение с ресурсами может привести к утечкам памяти, которые со временем снижают производительность приложения и вызывают сбои.


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

  1. Неправильное управление слушателями событий

Stencil позволяет добавлять слушатели с помощью декоратора @Listen. Если слушатели привязываются к глобальным объектам (например, window или document) и не снимаются при уничтожении компонента, память, занимаемая компонентом, не освобождается.

@Component({ tag: 'my-component' })
export class MyComponent {
  @Listen('resize', { target: 'window' })
  handleResize() {
    console.log('Window resized');
  }
}

В этом примере слушатель resize будет оставаться активным даже после удаления элемента из DOM, создавая утечку.

Решение: использовать локальные слушатели и вручную отписываться от глобальных событий в методе disconnectedCallback.

@Component({ tag: 'my-component' })
export class MyComponent {
  resizeHandler = () => console.log('Window resized');

  connectedCallback() {
    window.addEventListener('resize', this.resizeHandler);
  }

  disconnectedCallback() {
    window.removeEventListener('resize', this.resizeHandler);
  }
}

  1. Ссылки на DOM-элементы

Stencil предоставляет декоратор @Element(), который позволяет хранить ссылку на корневой элемент компонента. Хранение ссылок на дочерние элементы вне компонента или в глобальных переменных предотвращает сборку мусора.

@Component({ tag: 'my-component' })
export class MyComponent {
  @Element() el: HTMLElement;

  private nodes: HTMLElement[] = [];

  addNode(node: HTMLElement) {
    this.nodes.push(node); // если nodes глобальны, память не освобождается
  }
}

Рекомендация: избегать хранения DOM-элементов вне жизненного цикла компонента. Если необходимо, очищать массивы и ссылки в disconnectedCallback.


  1. Неправильная работа с таймерами и подписками

Использование setTimeout, setInterval и внешних подписок на RxJS или события также является частой причиной утечек:

@Component({ tag: 'timer-component' })
export class TimerComponent {
  intervalId: any;

  connectedCallback() {
    this.intervalId = setInterval(() => console.log('Tick'), 1000);
  }
}

Если не вызвать clearInterval в disconnectedCallback, интервал продолжит работать после удаления компонента.

disconnectedCallback() {
  clearInterval(this.intervalId);
}

Жизненный цикл компонентов и очистка ресурсов

Stencil предоставляет несколько методов жизненного цикла, которые позволяют контролировать создание и уничтожение компонентов:

  • connectedCallback() – вызывается при добавлении элемента в DOM, полезно для инициализации событий и подписок.
  • disconnectedCallback() – вызывается при удалении элемента из DOM, ключевой метод для очистки ресурсов.
  • componentWillLoad() и componentDidLoad() – вызываются перед и после первого рендера; удобны для начальной настройки, но не для очистки.

Правильное использование этих методов минимизирует риск утечек.


Инструменты диагностики

  1. Chrome DevTools Memory Profiler

Позволяет фиксировать снимки памяти, отслеживать количество объектов и идентифицировать объекты, которые не освобождаются после удаления компонента.

  1. Performance Timeline

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

  1. Automated tools

Инструменты вроде stencil-watch или cypress могут использоваться для стресс-тестов компонентов, выявляя рост потребления памяти при повторном создании и удалении элементов.


Практические рекомендации

  • Всегда отписывать глобальные события и таймеры в disconnectedCallback.
  • Не хранить DOM-элементы и объекты состояния вне компонента.
  • Использовать слабые ссылки (WeakMap, WeakSet) для кэширования, если объекты должны жить только столько, сколько компонент.
  • Проверять утечки с помощью профилировщика после добавления сложной логики и подписок.
  • Избегать циклических ссылок, которые мешают сборке мусора.

Особенности Stencil и памяти

Stencil использует виртуальный DOM и однонаправленный поток данных, что снижает вероятность утечек по сравнению с ручным управлением DOM. Однако динамическое создание элементов через render() или h() при отсутствии очистки может привести к накоплению объектов.

Пример безопасного рендеринга с динамическими элементами:

render() {
  const items = this.items.map(item => <li key={item.id}>{item.name}</li>);
  return <ul>{items}</ul>;
}

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