Angular 22: hidratación incremental por defecto y cache de recursos en SSR
Corriste ng update a Angular 22 y ahora la hidratación incremental está activa sin que toques una línea. Si tu app ya usaba provideClientHydration(), el commit "make incremental hydration default behavior" la encendió por ti. Y en la misma versión llegó la otra pieza que faltaba del puzzle SSR: cachear resultados de resource() en el TransferState. - Fuente: Angular v22.0.0 release notes
Las dos cosas resuelven el mismo tipo de problema: la distancia entre lo que el servidor renderiza y lo que el navegador reconstruye. Una ataca el JavaScript que cargas; la otra, las peticiones que repites. Vamos por partes.
Qué cambió exactamente en v22
Antes de v22, la hidratación incremental era opt-in. En v19 llegó como developer preview con withIncrementalHydration(), en v20 se volvió estable, y en v22 se convirtió en el comportamiento por defecto:
// main.ts
import {bootstrapApplication, provideClientHydration} from '@angular/platform-browser';
bootstrapApplication(App, {
providers: [provideClientHydration()],
});
Con esta única llamada, v22 te da tres cosas: hidratación del DOM, el transfer cache de HttpClient y la hidratación incremental. - Fuente: provideClientHydration API
Detalles que conviene saber:
withIncrementalHydration()quedó deprecado. Sigue funcionando en v22, pero la intención oficial es eliminarlo en v24. Si lo tienes, bórralo. - Fuente: withIncrementalHydration API- Event replay se activa solo. La hidratación incremental depende de event replay y lo habilita automáticamente. Si tenías
withEventReplay()en tu lista de providers, ya puedes quitarlo. - Fuente: Incremental hydration guide - ¿Quieres el comportamiento de antes? Existe un opt-out explícito:
// main.ts
import {provideClientHydration, withNoIncrementalHydration} from '@angular/platform-browser';
bootstrapApplication(App, {
providers: [provideClientHydration(withNoIncrementalHydration())],
});
Si vienes de mi guía de rendering en Angular, esto cambia el mapa: el modelo CSR/SSR/SSG sigue igual, pero el "híbrido" ya no requiere configuración.
Los triggers de hidratación
La hidratación incremental funciona con @defer y triggers de tipo hydrate. Cada trigger define una frontera de hidratación: el servidor renderiza el template principal, pero el navegador no descarga ni hidrata ese bloque hasta que el trigger se dispara.
<!-- dashboard.html -->
@defer (hydrate on interaction) {
<analytics-panel />
} @placeholder {
<div>Panel loading…</div>
}
El catálogo completo:
hydrate on idle: cuando el navegador está libre. Nuevo en v22: acepta timeout,@defer (hydrate on idle(500)). - Fuente: v22.0.0 release noteshydrate on viewport: cuando el bloque entra en pantalla.hydrate on interaction: click o keydown sobre el elemento.hydrate on hover: mouseover o focusin.hydrate on immediate: inmediatamente después del contenido no diferido.hydrate on timer(500ms): tras un tiempo determinado.hydrate when expr: cuando una expresión se vuelve truthy.hydrate never: nunca; el bloque queda como contenido estático en la carga inicial.
Puedes combinar triggers de carga con triggers de hidratación. En la carga inicial (SSR) manda el hydrate; en navegaciones client-side manda el trigger normal:
<!-- product-list.html -->
@defer (on idle; hydrate on interaction) {
<filters-panel />
} @placeholder {
<div>Filters</div>
}
Un detalle que se agradece: antes, un @defer above the fold intercambiaba placeholder por contenido y generaba layout shift. Con hidratación incremental, el servidor renderiza el template principal desde el inicio, así que el contenido está donde tiene que estar y la hidratación no mueve un pixel. La guía oficial lo resume en beneficios cualitativos: bundles iniciales más pequeños que mejoran FID y CLS. Cifras oficiales de LCP/INP no hay publicadas, así que mide en tu app antes de citar números. - Fuente: Incremental hydration guide
Cache de recursos en SSR: la opción id
Ahora la otra mitad. Cuando tu app renderiza en el servidor, un resource() corre su loader para producir el HTML inicial. Y durante la hidratación, el navegador corre el mismo loader otra vez. La misma petición, dos veces. En v22, la solución cabe en una línea:
// user-profile.ts
import {resource, Signal} from '@angular/core';
const userId: Signal<string> = getUserId();
const userResource = resource({
params: () => ({id: userId()}),
loader: ({params}) => fetchUser(params),
id: 'user-unique-id',
});
La opción id es nueva en BaseResourceOptions y estable desde v22.0. Funciona así: en el servidor, el valor resuelto se guarda en el TransferState y se serializa en el HTML inicial. En el cliente, si el dato está en el cache, el resource se inicializa directamente en estado 'resolved' y el loader nunca corre. - Fuente: Resource guide: Caching resource data with SSR
Código real que funciona: el usuario ve el perfil sin flash de "loading" y tu backend recibe una petición menos por visita. Reglas para que no te pegue:
- El
iddebe ser único dentro de tu aplicación e idéntico en servidor y cliente. Si no coinciden, el cache no se encuentra y el loader corre igual. - La advertencia de seguridad es seria: el valor cacheado se serializa en el HTML de la página. Si el HTML puede compartirse o cachearse entre usuarios, no pongas
iden resources que cargan datos específicos del usuario que disparó el render. - Fuente: misma fuente
¿Y httpResource? No necesita nada: va por HttpClient, que ya tiene su transfer cache por defecto desde v16. El id es para los loaders custom: fetch(), SDKs de terceros, snapshots de Firestore, cualquier cosa que no pase por HttpClient. - Fuente: SSR guide
Cómo se compone todo
Las piezas encajan así:
- Route-level render modes deciden qué rutas renderizan en el servidor. La hidratación incremental solo actúa donde hubo SSR: en navegaciones client-side, un
@deferconhydratese comporta como un@defernormal y se renderiza el@placeholder. Por eso sigue siendo obligatorio. - Fuente: Incremental hydration guide - Event replay encola los eventos que llegan antes de la hidratación y los repite después. Es lo que hace seguro el patrón
hydrate on interaction: el usuario puede hacer click antes de que el bloque esté vivo y el click no se pierde. - El cache de
idelimina el re-fetch en hidratación; el transfer cache de HttpClient hace lo propio con las peticiones que sí pasan porHttpClient.
Un cambio menor pero que te va a encontrar: provideServerRendering ahora limita las respuestas a 1 MB por defecto con el fetch backend. Si te pasas, error NG02825. Se ajusta con maxResponseBodySize. - Fuente: SSR guide
Checklist de migración v21 → v22
Del changelog y la guía de actualización:
ng update @angular/core@22 @angular/cli@22. Nada más para encender la hidratación incremental si ya teníasprovideClientHydration().- Borra
withIncrementalHydration()si lo usabas. Deprecado, fuera en v24. - Borra
withEventReplay()si lo tenías. Redundante ahora. - Agrega
ida tusresource()/rxResource()que quieras cachear en SSR. Recuerda la advertencia de datos por usuario. - Si sirves respuestas mayores a 1 MB en SSR, configura
maxResponseBodySizeenprovideServerRendering.
Tips para producción
- Empieza por los componentes pesados de abajo. Mapas, gráficas, editors: son los que más JS cargan y los que el usuario rara vez toca al aterrizar.
hydrate on idleohydrate on viewporty a medir. - Los triggers de interacción son para componentes sobre los que se puede hacer click antes de hidratar. Un botón "Cargar más" es el candidato perfecto: el click se encola y se repite.
- Un
idpor resource, documentado. Seis meses después nadie recordará por qué ese string es'user-unique-id'. Un comentario de tres palabras ahorra una investigación de tres horas. - Datos por usuario, sin
id. El TransferState termina en el HTML. Un carrito de compras cacheado en el HTML compartido es un ticket de soporte esperando a nacer. - Mide FID y CLS antes y después. Los beneficios documentados son cualitativos; tus números, los que valen.
Si aún no leíste cómo encaja esto en el panorama completo de rendering, empieza por CSR, SSR y SSG en Angular y vuelve. Y si quieres el resto de las novedades de la versión, ahí está mi repaso de Angular 22.
¿Cuántos kilobytes de JS te ahorraría hidratar por demanda tu página más pesada? Cuéntame en los comentarios.