Як тече пам’ять, якщо ви забудете скасувати підписку Observable

angular

У сучасних веб-додатках, що використовують RxJS (наприклад, у Angular), Observables широко застосовуються для асинхронної роботи. Якщо підписка на Observable не скасована, це може призвести до витоку пам’яті. У цій статті розглянемо, як саме відбувається витік, чому забута відписка викликає проблеми і як цьому запобігти.

Витік пам’яті

Витік пам’яті означає, що об’єкти, які більше не потрібні, залишаються в пам’яті та не знищуються збирачем сміття. У контексті Observables це відбувається, коли додаток продовжує зберігати посилання на підписувача, навіть після того, як компонент (чи інший об’єкт) вже знищений. Збирач сміття бачить, що цей об’єкт, який мав би бути видалений, все ще використовується, тому не звільняє його пам’ять.

Як працює Observable та підписка

Observable є джерелом асинхронних даних, яке надсилає значення або події своїм підписувачам. Поки триває підписка, Observable може надсилати нові дані в будь-який момент. Щоб зупинити надсилання даних, звичайно виконують одну з дій:

  • complete()
  • error()
  • unsubscribe()

Якщо Observable не завершується автоматично (наприклад, без кінця надсилає події), а розробник не викликає unsubscribe(), підписка лишається активною.

Чому забута відписка викликає проблеми

Якщо компонент підписався на Observable і забув відписатися, Observable далі триматиме посилання на цей компонент. Навіть коли компонент знищується (наприклад, користувач пішов на іншу сторінку), об’єкт компонента не може бути прибраний із пам’яті, оскільки існує активна підписка. Це призводить до накопичення непотрібних об’єктів і зростання використання пам’яті. З часом такий витік може викликати зниження продуктивності та інші збої.

Приклад в Angular

Нижче наведено простий приклад. Якщо компонент не викликає unsubscribe(), він ризикує залишити підписку активною і спровокувати витік пам’яті.

import { Component, OnInit, OnDestroy } from '@angular/core';
import { Subscription } from 'rxjs';
import { SomeService } from './some.service';

@Component({
  selector: 'app-example',
  template: `...`
})
export class ExampleComponent implements OnInit, OnDestroy {
  private subscription: Subscription;

  constructor(private someService: SomeService) {}

  ngOnInit() {
    this.subscription = this.someService.getData().subscribe(data => {
      console.log('Отримано дані:', data);
    });
  }

  ngOnDestroy() {
    // Якщо пропустити цю відписку, підписка залишиться активною
    this.subscription.unsubscribe();
  }
}

Коли забути відписатися

Уявімо, що у додатку є список компонентів ExampleComponent, який з’являється і зникає. Якщо кожен екземпляр створює нову підписку і не відписується, кожен об’єкт далі лишатиметься в пам’яті. Через деякий час це може викликати зависання програми або зростання споживання ресурсів.

Наслідки

  • Збільшення обсягу пам’яті. Додаток почне споживати дедалі більше ресурсів.
  • Погіршення продуктивності. Багато непотрібних об’єктів у пам’яті негативно впливають на швидкість.
  • Непередбачувана поведінка. Логіка може продовжувати виконуватися для об’єктів, яких фактично вже не існує в інтерфейсі.

Як уникнути витоків

Виклик unsubscribe()

Найпростіше рішення — запам’ятати Subscription і викликати unsubscribe() у методі життєвого циклу (наприклад, ngOnDestroy у Angular).

Використання takeUntil()

У RxJS можна застосувати підхід із Subject. Коли компонент знищується, Subject надсилає сигнал, і всі підписки з оператором takeUntil(this.destroy$) автоматично відписуються.

import { Component, OnInit, OnDestroy } from '@angular/core';
import { Subject } from 'rxjs';
import { takeUntil } from 'rxjs/operators';
import { SomeService } from './some.service';

@Component({
  selector: 'app-example',
  template: `...`
})
export class ExampleComponent implements OnInit, OnDestroy {
  private destroy$ = new Subject<void>();

  constructor(private someService: SomeService) {}

  ngOnInit() {
    this.someService.getData()
      .pipe(takeUntil(this.destroy$))
      .subscribe(data => {
        console.log('Отримано дані:', data);
      });
  }

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

При знищенні компонента destroy$.next() завершує всі підписки, що використовують takeUntil(this.destroy$).

Чистка в масиві

Якщо у компоненті є кілька підписок, їх можна зберігати у масив. У ngOnDestroy пройтися цим масивом і викликати unsubscribe() для кожної підписки.

Ознаки витоку

Якщо ви помічаєте:

  • Зростання використання пам’яті з часом
  • Логіка компонента викликається, хоча компонент уже не в інтерфейсі
  • Повільну роботу або зависання

Швидше за все, десь забули відписатися від Observable.

Висновок

Витік пам’яті через невідписані підписки — одна з найпоширеніших проблем в Angular та інших середовищах, де застосовуються Observables. Щоб цього уникнути, достатньо:

  • Використовувати unsubscribe() у життєвому циклі компонента
  • Або організувати takeUntil() із Subject
  • Правильно використовувати async пайп чи інші підходи для автоматичної відписки

Ці способи допомагають зберегти стабільність та продуктивність вашого застосунку, запобігаючи накопиченню непотрібних об’єктів у пам’яті.