Inicio / Artículos

Arranque no bloqueante de Angular: Carga de configuración con rxResource en su lugar

Aprenda por qué un APP_INITIALIZER de bloqueo deja a los usuarios de Angular en una página en blanco, y cómo rxResource, así como los resolvers de shell de aplicación y de rutas, cargan la configuración sin él.

Editor: REACTAPP.TOP5 min de lectura1112 palabras

Fuentes: Stop Blocking Your App Load: Architecting APP_INITIALIZER with Signals

Las aplicaciones empresariales con Angular suelen necesitar muchas cosas antes de mostrar la primera pantalla útil: indicadores de funcionalidades, permisos RBAC del usuario, tema del inquilino y un cliente de telemetría. La forma tradicional de obtenerlos es mediante un APP_INITIALIZER que devuelve una promesa, y Angular no renderiza nada hasta que esa promesa se resuelve. A continuación verá cuál es el costo de esto y cómo tratar la configuración como estado reactivo con rxResource, de modo que la página aparezca de inmediato y solo las partes que realmente dependen de la configuración esperen.

Por qué un inicializador bloqueante es perjudicial

La configuración clásica registra una fábrica bajo el proveedor múltiple APP_INITIALIZER. La fábrica devuelve una función que convierte la solicitud de configuración en una promesa, y el framework no iniciará al componente raíz hasta que esta se resuelva.

// 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
  }
]

El efecto es una aritmética sencilla: si el endpoint de configuración tarda 600 ms, los usuarios ven una página vacía durante al menos 600 ms en cada carga. Ese retraso se suma directamente al tiempo de First Contentful Paint, y un endpoint lento o que falla se convierte en una aplicación en blanco o defectuosa, sin posibilidad de mostrar un mensaje útil.

Hay dos detalles en este fragmento que ya están obsoletos. toPromise() está descontinuado en RxJS en favor de firstValueFrom(), y las versiones recientes de Angular descontinúan el token APP_INITIALIZER en favor de la función provideAppInitializer(). Consulte la documentación de la versión de Angular que utiliza. Sin embargo, ninguno de estos cambios elimina el comportamiento de bloqueo; solo modifican la sintaxis.

Tratar la configuración como una dependencia reactiva

La alternativa es dejar de tratar la configuración como un punto de control y comenzar a considerarla como datos que la interfaz de usuario espera recibir. La familia resource() de Angular expresa exactamente eso: un valor asíncrono cuyo estado de carga y error se exponen como señales.

Un servicio de configuración basado en rxResource

En lugar de entregarle a Angular una promesa, el servicio declara un recurso. rxResource toma un cargador que devuelve un Observable, en este caso la llamada a HttpClient, lo inicia cuando se crea el servicio y expone value, isLoading y error como señales. Nada espera a que se complete durante el proceso de inicialización.

// 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; }
}

Tenga en cuenta que los getters devuelven las señales mismas, por lo que los consumidores las llaman, como en configService.isLoading(). Las API de recursos han ido evolucionando con cada versión (en las versiones más recientes la opción rxResource se denomina stream en lugar de loader), así que debe adaptar los nombres de las opciones a su versión de Angular.

Manejo de la configuración sin la cual la app no puede ejecutarse

Algunos ajustes son realmente necesarios, como los valores requeridos para la autenticación. Aún no es necesario bloquear toda la app; existen dos lugares mejores donde esperar.

Opción 1: renderizar primero una carcasa

Por lo general, esta es la mejor opción. La carcasa (encabezado, navegación, diseño) se renderiza de inmediato, y el bloque de flujo de control @if muestra un esqueleto hasta que el recurso haya terminado de cargarse, momento en el cual toma el control la salida del router.

<!-- Non-blocking, instant render -->
@if (configService.isLoading()) {
  <app-skeleton-loader />
} @else {
  <router-outlet />
}

Los usuarios reciben una retroalimentación visual inmediata, y la plantilla es también un lugar adecuado para manejar los fallos. Añada una rama para configResource.error() con un botón de reintentar que llame a reload() del recurso; de lo contrario, una solicitud fallida terminará con la renderización de un outlet sin configuración.

Opción 2: un resolvedor de rutas

Si una ruta específica necesita configuración antes de activarse, un resolvedor limita la espera a esa navegación en lugar de al proceso completo de inicio.

export const configResolver: ResolveFn<AppConfig> = () => {
  return inject(ConfigService).configResource.value!;
};


// In app.routes.ts
{ path: 'dashboard', component: DashboardComponent, resolve: { config: configResolver } }

Tenga cuidado con esta implementación exacta. configResource.value es una señal, y la aserción de que no es nula solo silencia al compilador: el resolvedor devuelve inmediatamente la señal sin esperar la solicitud. Para realmente esperar, el resolvedor debe devolver algo que se complete una vez que existan los datos, por ejemplo un Observable derivado del recurso que filtre el estado de carga, o una llamada directa a HttpClient envuelta en firstValueFrom().

Qué obtiene el enfoque no bloqueante

  • Pintado inicial más rápido. El navegador puede renderizar el HTML básico y el CSS fundamental de inmediato en lugar de un elemento raíz vacío, lo que mejora el First Contentful Paint y las métricas que monitorean los motores de búsqueda y los paneles de control.
  • Rendimiento percibido mejor. Los esqueletos informan a los usuarios de que la aplicación está activa mientras las banderas y permisos se cargan en segundo plano.
  • Resiliencia ante errores. Un endpoint de configuración defectuoso ya no interrumpe el inicio de la aplicación. La app puede mostrar un estado de error, ofrecer reintentar y mantener las partes no relacionadas del panel de control utilizables.
  • Hay un compromiso que aceptar: cada componente que lee la configuración debe ahora manejar el estado de “aún no cargado”, o encontrarse detrás de un mecanismo (el @if del shell o un resolvedor) que lo garantice. Para modelar esos estados de manera clara, una unión discriminada funciona bien, como se muestra en sustituyendo las banderas de carga por una unión AsyncState.

    Conclusión

    • Un inicializador que devuelve una promesa mantiene toda la aplicación retenida por la solicitud de inicio más lenta.
    • rxResource convierte la configuración en señales de valor, carga y error a las que los templates pueden reaccionar.
    • Un shell de aplicación con un esqueleto @if es el predeterminado; un resolvedor de rutas se utiliza para aquellas que no deben activarse sin datos, siempre y cuando realmente espere.
    • Mantenga los inicializadores para tareas que realmente deben completarse antes de que se muestre cualquier contenido, como registrar un manejador de errores global.

    Tanto si elige un shell global como resolvedores por ruta, el principio es el mismo: hacer que la espera se muestre como un estado de UI local visible en lugar de una página en blanco.

    Lecturas relacionadas

  • Más rápido Angular haciendo menos: diez técnicas más allá de OnPush y trackBy — Reduce el uso innecesario de la red, así como el trabajo en el entorno reactivo, DOM y CPU en Angular moderno mediante @defer, hidratación incremental, resource(), señales computadas, fases de renderizado y Web Workers.
  • Runas de Svelte 5 y señales de SolidJS: actualizaciones de UI sin volver a renderizar — Conoce cómo Svelte 5 compila runas y SolidJS conecta señales para actualizar el DOM directamente, cómo esto difiere de React y Angular, y cuándo vale la pena hacer el cambio.