Безблокирующий запуск Angular: загрузка конфигурации с помощью rxResource вместо обычного способа
Узнайте, почему использование блокирующего метода APP_INITIALIZER приводит к тому, что пользователи Angular оказываются на пустой странице, и как rxResource, а также компоненты app shell и резолверы маршрутов загружают конфигурацию без него.
Источники: Stop Blocking Your App Load: Architecting APP_INITIALIZER with Signals
В корпоративных приложениях на Angular обычно требуется много элементов до появления первого полезного экрана: флаги функций, права доступа пользователя по модели RBAC, тема арендатора, клиент для сбора телеметрии. Традиционный способ реализации этого — использование APP_INITIALIZER, возвращающего обещание, и Angular не отрисовывает ничего до тех пор, пока это обещание не будет выполнено. Ниже вы увидите, во что это обходится, и как рассматривать конфигурацию как реактивное состояние с помощью rxResource, чтобы страница отображалась сразу, а ожидали только те части, которые действительно зависят от конфигурации.
Почему блокирующий инициализатор вреден
В классической настройке фабрика регистрируется под многопровайдером APP_INITIALIZER. Фабрика возвращает функцию, преобразующую запрос к конфигурации в обещание, и фреймворк не запустит корневой компонент до его выполнения.
// The Blocking Legacy Pipeline
export function initializeConfig(configService: ConfigService) {
// Angular halts all execution until this Promise resolves
return () => configService.loadConfiguration().toPromise();
}
// In app.config.ts
providers: [
{
provide: APP_INITIALIZER,
useFactory: initializeConfig,
deps: [ConfigService],
multi: true
}
]
Эффект заключается в простых арифметических расчётах: если время обработки конфигурационного эндпоинта составляет 600 мс, пользователи видят пустую страницу в течение как минимум 600 мс при каждой загрузке. Эта задержка напрямую влияет на показ первого содержимого страницы, и медленный или неисправный эндпоинт приводит к появлению пустого или неисправного приложения без возможности отобразить полезное сообщение.
Два аспекта, упомянутые в этом фрагменте, устарели. В RxJS функция toPromise() заменена на firstValueFrom(), а в новых версиях Angular токен APP_INITIALIZER заменён на функцию provideAppInitializer(). Проверьте документацию к версии Angular, которую вы используете. Однако ни одно из этих изменений не устраняет проблему блокировки выполнения; они лишь меняют синтаксис.
Обработка конфигурации как реактивной зависимости
Альтернатива заключается в том, чтобы перестать рассматривать конфигурацию как барьер и начать рассматривать её как данные, ожидаемые интерфейсом. Семейство методов resource() в Angular именно это и представляет: асинхронное значение, состояния загрузки и ошибок которого выдаются в виде сигналов.
Сервис конфигурации, построенный на rxResource
Вместо того чтобы передавать Angular обещание, сервис объявляет ресурс. rxResource берёт загрузчик, возвращающий Observable — здесь это вызов HttpClient — запускает его при создании сервиса и выдаёт value, isLoading и error в виде сигналов. Во время запуска ничего не ждёт его выполнения.
// The Non-Blocking Reactive Architecture
@Injectable({ providedIn: 'root' })
export class ConfigService {
private http = inject(HttpClient);
// Define a resource that doesn't block the bootstrap
configResource = rxResource({
loader: () => this.http.get<AppConfig>('/api/config')
});
// Expose as a signal for easy template consumption
get config() { return this.configResource.value; }
get isLoading() { return this.configResource.isLoading; }
}
Обратите внимание, что геттеры возвращают сами сигналы, поэтому их вызывают потребители, как в примере configService.isLoading(). API ресурсов постоянно развиваются с каждой новой версией (в более новых версиях параметр rxResource называется stream, а не loader), поэтому имя параметра должно соответствовать версии Angular.
Работа с конфигурацией, без которой приложение не может работать
Некоторые настройки действительно необходимы, например значения, требуемые для аутентификации. Однако нет необходимости блокировать всё приложение; существуют два более подходящих способа ожидания.
Вариант 1: сначала отрендерить оболочку
Обычно это лучший выбор. Оболочка (заголовок, навигация, макет) отрендеряется сразу, а блок управления @if отображает «скелет» до завершения загрузки ресурса, после чего берет управление на себя элемент router outlet.
<!-- Non-blocking, instant render -->
@if (configService.isLoading()) {
<app-skeleton-loader />
} @else {
<router-outlet />
}
Пользователи получают немедленную визуальную обратную связь, и шаблон также является подходящим местом для обработки ошибок. Добавьте ветку для configResource.error() с кнопкой повторной попытки, которая вызывает метод reload() ресурса; в противном случае неудачный запрос приводит к отображению контента без конфигурации.
Вариант 2: резолвер маршрутов
Если определенному маршруту требуется конфигурация перед его активацией, резолвер ограничивает ожидание именно этим переходом, а не всем процессом запуска приложения.
export const configResolver: ResolveFn<AppConfig> = () => {
return inject(ConfigService).configResource.value!;
};
// In app.routes.ts
{ path: 'dashboard', component: DashboardComponent, resolve: { config: configResolver } }
Будьте осторожны с этой конкретной реализацией. configResource.value является сигналом, и утверждение о том, что значение не равно null, лишь заглушает предупреждения компилятора: резолвер сразу же возвращает этот сигнал, не дожидаясь запроса. Чтобы действительно ждать, резолвер должен возвращать объект, который будет готов к использованию появлением данных, например Observable, полученный от ресурса и фильтрующий состояние загрузки, или прямой вызов HttpClient, обернутый в метод firstValueFrom().
Преимущества неблокирующего подхода
- Более быстрая отрисовка. Браузер может сразу же отрисовать основной HTML-код и базовые стили CSS вместо пустого корневого элемента, что улучшает показатели First Contentful Paint и другие метрики, отслеживаемые поисковыми системами и панелями управления.
Существует определенный компромисс: каждый компонент, который читает конфигурацию, теперь должен обрабатывать состояние «еще не загружено», либо находиться за барьером (используя @if в шелле или механизм разрешения), который гарантирует правильную работу. Для чистой моделировки таких состояний хорошо подходит дискриминированное объединение, как показано в статье о замене флагов загрузки на объединение AsyncState.
Заключение
- Инициализатор, возвращающий обещание, заставляет всё приложение ждать самого медленного запроса на запуск.
rxResourceпреобразует конфигурацию в сигналы значений, процесса загрузки и ошибок, на которые могут реагировать шаблоны.- По умолчанию используется оболочка приложения с шаблоном
@if; резолвер маршрутов подходит для маршрутов, которые не должны активироваться без данных, при условии что он действительно ждёт их. - Используйте инициализаторы для задач, которые действительно должны быть завершены до отрисовки любого контента, например для регистрации глобального обработчика ошибок.
Независимо от того, выберете ли вы глобальную оболочку или резолверы по маршрутам, принцип остаётся тем же: превратите процесс ожидания в видимое состояние пользовательского интерфейса, а не в пустую страницу.