Non-Blocking Angular Startup: Loading Config with rxResource Instead
Learn why a blocking APP_INITIALIZER leaves Angular users on a blank page, and how rxResource, an app shell and route resolvers load configuration without it.
This article is published in English.
Sources: Stop Blocking Your App Load: Architecting APP_INITIALIZER with Signals
Enterprise Angular apps usually need a lot before the first useful screen: feature flags, the user's RBAC permissions, the tenant's theme, a telemetry client. The traditional way to get them is an APP_INITIALIZER that returns a promise, and Angular renders nothing until that promise settles. Below you will see what that costs, and how to treat configuration as reactive state with rxResource, so the page appears immediately and only the parts that truly depend on configuration wait.
Why a blocking initializer hurts
The classic setup registers a factory under the APP_INITIALIZER multi-provider. The factory returns a function that converts the config request into a promise, and the framework will not bootstrap the root component until it resolves.
// 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
}
]
The effect is simple arithmetic: if the config endpoint takes 600 ms, users look at an empty page for at least 600 ms, on every load. That delay goes straight into First Contentful Paint, and a slow or failing endpoint turns into a blank or broken app with no chance to show a helpful message.
Two details in this snippet have aged. toPromise() is deprecated in RxJS in favor of firstValueFrom(), and recent Angular versions deprecate the APP_INITIALIZER token in favor of the provideAppInitializer() function. Check the documentation for the Angular version you run. Neither change removes the blocking behavior, though; they only change the syntax.
Treating configuration as a reactive dependency
The alternative is to stop treating configuration as a gate and start treating it as data the UI expects to arrive. Angular's resource() family expresses exactly that: an asynchronous value with its loading and error state exposed as signals.
A config service built on rxResource
Rather than handing Angular a promise, the service declares a resource. rxResource takes an Observable-returning loader, here the HttpClient call, starts it when the service is created, and exposes value, isLoading and error as signals. Nothing waits on it during bootstrap.
// 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; }
}
Note that the getters return the signals themselves, so consumers call them, as in configService.isLoading(). The resource APIs have been evolving across releases (in newer versions the rxResource option is named stream rather than loader), so match the option names to your Angular version.
Handling configuration the app cannot run without
Some settings are genuinely required, for instance values needed for authentication. Blocking the whole app is still not necessary; there are two better places to wait.
Option 1: render a shell first
This is usually the best choice. The shell (header, navigation, layout) renders at once, and the @if control flow block shows a skeleton until the resource has finished loading, after which the router outlet takes over.
<!-- Non-blocking, instant render -->
@if (configService.isLoading()) {
<app-skeleton-loader />
} @else {
<router-outlet />
}
Users get immediate visual feedback, and the template is a natural place to handle failure too. Add a branch for configResource.error() with a retry button that calls the resource's reload(); otherwise a failed request ends in an outlet rendering without configuration.
Option 2: a route resolver
If a specific route needs configuration before it activates, a resolver scopes the wait to that navigation instead of the whole bootstrap.
export const configResolver: ResolveFn<AppConfig> = () => {
return inject(ConfigService).configResource.value!;
};
// In app.routes.ts
{ path: 'dashboard', component: DashboardComponent, resolve: { config: configResolver } }
Be careful with this exact implementation. configResource.value is a signal, and the non-null assertion only silences the compiler: the resolver hands the signal back immediately without waiting for the request. To really wait, the resolver must return something that completes once the data exists, for example an Observable derived from the resource that filters out the loading state, or a direct HttpClient call wrapped with firstValueFrom().
What the non-blocking approach gains
- Faster first paint. The browser can render the shell HTML and base CSS right away instead of an empty root element, which improves First Contentful Paint and the metrics that search engines and dashboards track.
- Better perceived performance. Skeletons tell users the app is alive while flags and permissions load in the background.
- Resilience to errors. A failing configuration endpoint no longer breaks bootstrap. The app can show an error state, offer a retry, and keep unrelated parts of the dashboard usable.
There is a trade-off to accept: every component that reads configuration must now handle the "not loaded yet" state, or sit behind a boundary (the shell's @if or a resolver) that guarantees it. For modeling those states cleanly, a discriminated union works well, as shown in replacing loading flags with an AsyncState union.
Wrapping up
- A promise-returning initializer holds the entire app hostage to your slowest startup request.
rxResourceturns configuration into signals for value, loading and error that templates can react to.- An app shell with an
@ifskeleton is the default; a route resolver fits routes that must not activate without data, provided it actually waits. - Keep initializers for work that truly must finish before anything renders, such as registering a global error handler.
Whether you choose a global shell or per-route resolvers, the principle is the same: make waiting a visible, local UI state instead of a blank page.