AI Angular и память: как не создавать утечки

Регистрация
23 Август 2023
Сообщения
2 819
Лучшие ответы
0
Реакции
0
Баллы
51
Offline
#1


Фронтенд-разработка в последние годы стала сложнее. Одностраничные приложения живут часами, пользователи открывают вкладки и оставляют их работать, данные приходят с серверов постоянно. В этом хаосе часто кажется, что главное - чтобы компонент рендерился, а Observable выдавал данные.

Но даже в самом аккуратном коде могут появляться утечки памяти. Утечка памяти возникает, когда объекты, которые больше не нужны, остаются в памяти, потому что на них ещё есть ссылки. Для браузера они живы, сборщик мусора их не трогает.

Для Angular-разработчика это важно, потому что:


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


  2. Проблему сложно отследить. Часто она возникает на долгих сессиях или в специфических сценариях.


  3. Даже новые возможности Angular, такие как signals и автоматическое управление отписками, не избавляют от ответственности.

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

Как работает память в браузере


Когда мы говорим о памяти в JavaScript, важно понимать два основных пространства: стек и куча.

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

Куча хранит объекты, массивы, функции и DOM-элементы. Доступ к данным из кучи медленнее, но объекты могут жить дольше. Сборщик мусора периодически проверяет кучу, используя алгоритм mark-and-sweep. Он отмечает объекты, на которые есть ссылки, и удаляет все остальные.

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

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

1. Подписки на Observable


Проблема: подписка хранит ссылку на компонент через callback. Компонент не удаляется, пока поток активен.

ngOnInit() {
this.service.data$.subscribe(d => this.value = d);
}

Как это работает в памяти:


  1. Observable хранит callback в куче.


  2. Callback захватывает контекст компонента (this).


  3. Сборщик мусора видит, что на компонент есть ссылка через callback → объект остаётся живым.

Решение:


  • Использовать async пайп в шаблоне для автоматического управления подпиской.


  • Для Angular <16 использовать takeUntil с Subject:

private destroy$ = new Subject<void>();

ngOnInit() {
this.service.data$
.pipe(takeUntil(this.destroy$))
.subscribe(d => this.value = d);
}

ngOnDestroy() {
this.destroy$.next();
this.destroy$.complete();
}

  • Для Angular 16+ использовать takeUntilDestroyed с DestroyRef:

this.service.data$
.pipe(takeUntilDestroyed(this.destroyRef))
.subscribe(d => this.value = d);
2. Таймеры и setInterval


Проблема: setInterval хранит callback, который ссылается на компонент. Компонент остаётся живым, пока таймер не остановлен.

setInterval(() => this.loadData(), 5000);

В памяти:


  • Таймер в куче → callback → компонент


  • GC не может удалить компонент, пока callback жив

Решение: очищать таймеры в ngOnDestroy/DestroyRef

private oldTimer: any;

// ===== Современный подход =====
private destroyRef = inject(DestroyRef);

ngOnInit() {
// Старый способ с ngOnDestroy
this.oldTimer = setInterval(() => this.loadDataOld(), 5000);

// Новый способ с DestroyRef
const newTimer = setInterval(() => this.loadDataNew(), 5000);
this.destroyRef.onDestroy(() => clearInterval(newTimer));
}

// При использовании angular < 16
ngOnDestroy() {
clearInterval(this.oldTimer);
}
3. Слушатели DOM


Проблема: addEventListener создаёт сильную ссылку на callback. Если callback ссылается на компонент или объекты данных, они остаются живыми после удаления DOM-узла.

document.addEventListener('scroll', this.onScroll);

В памяти:


  • DOM → callback → компонент


  • Компонент и связанные данные остаются живыми

Решение:


  • Использовать Angular-события через шаблон:

<div (scroll)="onScroll($event)"></div>

  • Или очищать вручную:

ngOnDestroy() {
document.removeEventListener('scroll', this.onScroll);
}
4. Singleton-сервисы


Проблема: сервис живёт столько же, сколько приложение. Если хранить в нём компоненты, массивы или объекты с ссылками на DOM, GC не сможет удалить их.

@Injectable({ providedIn: 'root' })
export class CacheService {
bigList: any[] = [];
}

В памяти:


  • Сервис → bigList → объекты → ссылки на компоненты


  • Всё остаётся живым до конца жизни приложения

Решение:


  • Очищать массивы/объекты, когда они больше не нужны
5. Замыкания


Проблема: функция захватывает контекст с большими объектами. Пока существует callback, на который есть ссылка, сборщик мусора не может удалить объект из кучи, даже если визуально компонент или элемент уже удалён.

const bigData = new Array(100000).fill('data');
document.body.onclick = () => console.log(bigData.length);

В памяти:


  • Callback → замыкание → bigData


  • Объект остаётся в памяти, потому что на него есть ссылка через замыкание.

Решение:

Разрывать ссылки на большие объекты


  • Не храните массивы, объекты или компоненты в переменных, которые будут захвачены замыканием.


  • Обнуляйте ссылки после использования, если они больше не нужны.

let bigData = new Array(100000).fill('data');

const handler = () => {
console.log(bigData.length);
bigData = null; // разрываем ссылку, GC сможет собрать объект
};

document.body.addEventListener('click', handler);
Учитывая все вышеизложенное можно составить краткий чеклист для предотвращения утечек:


  • Использовать async пайп для Observable, чтобы автоматически управлять подписками.


  • Отписываться от потоков через takeUntil (для старых версий) или takeUntilDestroyed с DestroyRef (для Angular 16+).


  • Очищать таймеры и слушатели в ngOnDestroy или через Renderer2.listen с автоматическим управлением.


  • Не хранить тяжёлые объекты, компоненты или ссылки на DOM в singleton-сервисах.


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


  • Регулярно проверять состояние памяти и профилировать приложение с помощью DevTools и Performance профайлера.
Подведем итоги


Память — это не «дополнительная тема», это фундамент стабильного приложения.

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

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

Каждый подписанный Observable, каждый таймер, каждый обработчик DOM - потенциальная точка утечки. И чем раньше это учитывать, тем проще избежать проблем в продакшене.
 
Сверху Снизу