В версии Angular 21.1 появилась экспериментальная функция маршрутизатора withExperimentalAutoCleanupInjectors. Эта настройка для решения давней, печально известной архитектурной особенности, связанной с управлением памятью при навигации между страницами.

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

Загвоздка заключается в том, что до выхода версии 21.1 такие инжекторы маршрутов никогда не уничтожались по умолчанию. Когда пользователь уходил со страницы, визуально контент исчезал, компоненты уничтожались, но сервисы самого маршрута продолжали бесконечно висеть в оперативной памяти браузера. Из-за этого приложение со временем потребляло все больше ресурсов, что приводило к классическим утечкам памяти. Фоновые задачи, запущенные внутри таких сервисов, например, интервальные таймеры или долгие подписки, продолжали свою работу вхолостую. Кроме того, современные реактивные инструменты Angular, такие как функция takeUntilDestroyed, работали некорректно в сервисах уровня маршрута, так как сам сервис формально никогда не удалялся из памяти фреймворка.

Как эта проблема решалась ранее

В прошлых версиях фреймворка нам приходилось бороться с этим поведением вручную. Поскольку сервис маршрута не уничтожался автоматически при смене URL, мы не могли просто положиться на его жизненный цикл. Приходилось создавать связки между жизненным циклом компонента-контейнера и сервисом, перенося ответственность за очистку ресурсов на сам компонент.

Чаще всего это выглядело как ручное управление подписками с использованием дополнительных сущностей из RxJS, таких как Subject. Мы были вынужден внедрять хук ngOnDestroy в компонент страницы и явно сообщать сервису, что пора остановить фоновую работу.

import { Injectable } from '@angular/core';
import { interval, Subject } from 'rxjs';
import { takeUntil } from 'rxjs/operators';

@Injectable()
export class RoutePollingService {
  private destroy$ = new Subject<void>();

  constructor() {
    interval(1000)
      .pipe(takeUntil(this.destroy$))
      .subscribe(() => {
        console.log('Запрашиваю новые цены на акции...');
      });
  }

  // Этот метод приходилось обязательно вызывать из ngOnDestroy компонента
  stopPolling() {
    this.destroy$.next();
    this.destroy$.complete();
  }
}

При таком подходе компонент страницы должен был получить экземпляр этого сервиса и перед своим уничтожением вызвать метод stopPolling(). Если разработчик забывал это сделать, таймер продолжал тикать вечно. Это плодило лишний шаблонный код, нарушало инкапсуляцию логики внутри сервиса и сильно увеличивало вероятность человеческой ошибки.

Как проблема решается теперь

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

Для включения этого поведения достаточно добавить одну настройку при инициализации маршрутизатора в главной конфигурации вашего приложения в файле app.config.ts. Вы просто импортируете функцию withExperimentalAutoCleanupInjectors и передаете ее внутрь provideRouter.

import { ApplicationConfig } from '@angular/core';
import { provideRouter, withExperimentalAutoCleanupInjectors } from '@angular/router';
import { routes } from './app.routes';

export const appConfig: ApplicationConfig = {
  providers: [
    provideRouter(routes, withExperimentalAutoCleanupInjectors())
  ]
};

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

import { Injectable } from '@angular/core';
import { interval } from 'rxjs';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';

@Injectable()
export class RoutePollingService {
  constructor() {
    // Подписка оборвется автоматически при уходе со страницы
    interval(1000)
      .pipe(takeUntilDestroyed())
      .subscribe(() => {
        console.log('Запрашиваю новые цены на акции...');
      });
  }
}

Как только пользователь покидает страницу маршрута, Angular корректно удаляет инжектор. Функция takeUntilDestroyed замечает это событие и моментально обрывает подписку на таймер. В результате фоновая работа полностью останавливается, память освобождается, а код становится максимально чистым и лаконичным. Также стоит отметить, что при использовании продвинутых стратегий кэширования через RouteReuseStrategy фреймворк проявляет гибкость и не удаляет сервисы тех страниц, которые вы решили явно сохранить в памяти для повторного быстрого отображения.