Când pagina vine randată de pe server (SSR sau prerender), browserul are deja DOM-ul complet. Fără hidratare, Angular ar porni ca într-o aplicație CSR: ar arunca acel DOM și l-ar construi din nou — clipire vizibilă, LCP stricat, layout shift. Hidratarea înseamnă că Angular parcurge DOM-ul existent, îl potrivește cu arborele de componente și îl refolosește: leagă binding-urile și atașează listenerii la nodurile care există deja.
Peste ea s-au construit două optimizări: event replay — click-urile făcute înainte de hidratare nu se pierd — și hidratarea incrementală — bucăți din pagină rămân HTML static, fără JS descărcat, până când chiar e nevoie de ele.
- Hidratarea refolosește HTML-ul trimis de server în loc să-l recreeze, deci pagina nu clipește la pornire.
- Event replay ține minte click-urile făcute înainte să se încarce JavaScript-ul și le rulează după.
- Hidratarea incrementală (
@defercuhydrate on …) lasă părți din pagină neinteractive până când e nevoie de ele. Codul lor se descarcă abia atunci. - HTML-ul de pe server și cel din browser trebuie să fie identice. Altfel apar erori de nepotrivire, cu coduri de tip
NG05xx. ngSkipHydratione ieșirea de urgență pentru o componentă care modifică DOM-ul direct.
provideClientHydration()pornește implicit hidratarea incrementală, iar ea activează automat event replay.withIncrementalHydration()e deprecated (eliminare planificată în v24) — scoate-l din config, e deja implicit.- Opțiunea nouă de opt-out e
withNoIncrementalHydration().
Ce face hidratarea, pas cu pas
Pe server, după randare, Angular adnotează HTML-ul ca să poată fi „regăsit” mai târziu: atributul ngh pe gazda fiecărei componente (un index în datele de hidratare), comentarii-ancoră pentru @if/@for/@defer, un comentariu HTML nghm ca marcaj de integritate, jsaction pe elementele care au listeneri și un <script id="ng-state"> cu datele de hidratare (__nghData__) și răspunsurile HTTP din transfer cache.
@Component({
selector: 'app-product-card',
template: `
<h2>{{ product().name }}</h2>
@if (product().stock > 0) {
<button (click)="addToCart()">Adaugă în coș</button>
}
`,
})
export class ProductCard {
readonly product = input.required<Product>();
addToCart() { /* ... */ }
}În browser, la bootstrap, Angular nu mai creează noduri: le caută pe cele existente, folosind adnotările. Pentru fiecare nod verifică tipul și tag-ul (nu și textul) și atașează listenerii. Un text diferit nu e eroare — e pur și simplu suprascris la primul change detection.
După ce aplicația devine stabilă (fără request-uri HTTP și PendingTasks în curs), urmează etapa de final: Angular șterge nodurile de pe server pe care nu le-a revendicat nimeni, reia evenimentele din coadă și oprește transfer cache-ul. De aceea o aplicație care nu devine stabilă are efecte vizibile chiar dacă DOM-ul a fost refolosit corect.
| Fără hidratare | Hidratare completă | Hidratare incrementală | |
|---|---|---|---|
| DOM-ul serverului | aruncat și recreat | refolosit | refolosit |
| JS necesar la pornire | tot | tot | doar pentru ce e în afara blocurilor @defer (hydrate …) |
| Când devine interactiv un bloc | după re-randare | la bootstrap, toată pagina odată | la trigger-ul blocului |
| Click-uri înainte de JS | pierdute | reluate doar cu withEventReplay() | reluate; un click pe un bloc deshidratat îl hidratează |
provideClientHydration și opțiunile
Dacă ai adăugat SSR cu ng add @angular/ssr, provider-ul e deja în app.config.ts. Fără argumente, în v22, primești setul recomandat:
import { ApplicationConfig } from '@angular/core';
import { provideClientHydration } from '@angular/platform-browser';
export const appConfig: ApplicationConfig = {
providers: [
// v22, fără argumente:
// ✓ hidratare non-distructivă (refolosește DOM-ul serverului)
// ✓ HTTP transfer cache (GET/HEAD făcute pe server nu se repetă în browser)
// ✓ hidratare incrementală (@defer cu hydrate …) — include event replay
// ✓ în dev mode: provideStabilityDebugging()
provideClientHydration(),
],
};
// app.config.server.ts face mergeApplicationConfig(appConfig, serverConfig),
// deci provider-ul ajunge și pe server — obligatoriu, altfel apare NG0505.| Funcție | Ce face | În v22 |
|---|---|---|
provideClientHydration() | hidratare non-distructivă + HTTP transfer cache + hidratare incrementală (cu event replay) | setul implicit |
withNoIncrementalHydration() | oprește hidratarea incrementală (și, odată cu ea, event replay) | nou în v22 |
withEventReplay() | event replay fără hidratare incrementală | inclus implicit; explicit doar cu opt-out-ul de mai sus |
withI18nSupport() | hidratează și componentele cu blocuri i18n; fără el sunt re-randate de la zero | opțional |
withHttpTransferCacheOptions({…}) | configurează ce răspunsuri ajung în HTML: filter, includeHeaders, includePostRequests, includeRequestsWithAuthHeaders… | opțional |
withNoHttpTransferCache() | oprește transfer cache-ul: fiecare GET se face de două ori | opțional |
withIncrementalHydration() | pornea hidratarea incrementală | deprecated din v22 (eliminare planificată în v24) |
Provider-ul trebuie să existe și în config-ul de server — app.config.server.ts generat de CLI face mergeApplicationConfig(appConfig, serverConfig), deci e acolo automat. Dacă lipsește, serverul nu adnotează HTML-ul, iar în browser pagina e re-randată de la zero (în dev mode, cu avertismentul NG0505). Combinațiile contradictorii (de exemplu withNoHttpTransferCache() cu withHttpTransferCacheOptions()) aruncă eroarea NG05001 în dev mode.
Event replay
Problema: HTML-ul de pe server e vizibil cu mult înainte să sosească JS-ul (în timeline-ul de pe pagina SSR e banda galbenă). Utilizatorul vede un buton și apasă — dar butonul nu are încă niciun listener.
Mecanismul se bazează pe JSAction, o bibliotecă de event delegation integrată în Angular:
- Marcare — la serializare, serverul pune
jsaction="click:;"pe fiecare element care are un listener în template. - Captare — un script mic, inline, injectat imediat după
<body>(event contract), ascultă la nivelulbodytipurile de evenimente folosite în pagină și le ține în memorie. - Reluare — după hidratare, când aplicația devine stabilă, Angular trimite evenimentele din coadă, în ordine, handler-elor reale.
Funcționează pentru evenimente native ale browserului (click, mouseover, focusin etc.). Cu hidratarea incrementală, contractul rămâne activ și după hidratare, ca să prindă evenimentele din blocurile încă deshidratate.
- Apasă „Reîncarcă pagina”, apoi dă click pe butoane cât timp bara de JS nu e plină.
Replay-ul salvează acțiunea, dar butonul nu reacționează vizual până la hidratare. Stările CSS (:hover, :active, :focus-visible) funcționează fără JS — folosește-le, ca utilizatorul să știe că click-ul „a prins”.
Hidratare incrementală cu @defer
Un @defer obișnuit randează pe server doar @placeholder-ul: trigger-ele nu rulează pe server. Un trigger hydrate schimbă asta: serverul încarcă dependențele blocului și randează conținutul principal. În browser, blocul rămâne deshidratat — HTML-ul se vede, dar chunk-ul JS al componentelor din el nici măcar nu e descărcat — până când se declanșează trigger-ul. Atunci Angular descarcă chunk-ul, hidratează blocul și reia evenimentele din coadă.
Două consecințe practice. Prima: poți pune @defer și deasupra fold-ului fără layout shift, pentru că pe ecran e deja conținutul final, nu placeholder-ul. A doua: un eveniment pe un element cu listener dintr-un bloc deshidratat îi declanșează hidratarea, oricare ar fi trigger-ul declarat — click-ul nu se pierde. Excepție face hydrate never: subarborele lui nu e deloc adnotat, deci nu are ce relua.
<app-header /> <!-- fără @defer: hidratat la bootstrap -->
@defer (hydrate on interaction) {
<app-gallery [images]="product().images" /> <!-- JS-ul galeriei: abia la primul click -->
} @placeholder {
<div class="gallery-skeleton"></div> <!-- folosit doar la navigare client-side -->
}
@defer (hydrate on hover) {
<app-recommendations [productId]="id()" />
} @placeholder {
<div class="reco-skeleton"></div>
}
@defer (hydrate on idle) {
<app-support-chat />
} @placeholder {
<button>Chat</button>
}
@defer (hydrate on viewport) {
<app-reviews [productId]="id()" /> <!-- sub fold: JS când intră în ecran -->
} @placeholder {
<div class="reviews-skeleton"></div>
}
@defer (hydrate never) {
<app-footer /> <!-- HTML static pentru totdeauna -->
}fără @defer (main.js)deshidratat48 KB, nedescărcathydrate on interactiondeshidratat36 KB, nedescărcathydrate on hoverdeshidratat22 KB, nedescărcathydrate on idledeshidratat30 KB, nedescărcat↓ derulează până la recenzii ↓
hydrate on viewportdeshidratat64 KB, nedescărcathydrate neverstatic, fără JS0 KB (ar fi 18)- Simularea pornește când demo-ul intră în ecran (sau apasă „Reîncarcă”).
Trigger-ele de hidratare
@defer (hydrate on idle) { … } <!-- requestIdleCallback -->
@defer (hydrate on idle(500)) { … } <!-- idle, dar cel târziu după 500 ms -->
@defer (hydrate on viewport) { … } <!-- conținutul blocului intră în ecran -->
@defer (hydrate on viewport({ rootMargin: '200px' })) { … } <!-- doar opțiuni literale -->
@defer (hydrate on interaction) { … } <!-- click / keydown în bloc -->
@defer (hydrate on hover) { … } <!-- mouseover / focusin în bloc -->
@defer (hydrate on immediate) { … } <!-- imediat după restul paginii -->
@defer (hydrate on timer(2s)) { … } <!-- ms sau s -->
@defer (hydrate when cartReady()) { … } <!-- condiție din componenta părinte -->
@defer (hydrate never) { … } <!-- niciodată; fără alte hydrate triggers -->
@defer (hydrate on viewport; hydrate on timer(5s)) { … } <!-- mai multe = oricare (OR) -->
<!-- ✗ erori de compilare -->
@defer (hydrate on interaction(btn)) { … } <!-- interaction/hover nu primesc referință -->
@defer (hydrate on viewport({ trigger: el })) { … }
@defer (hydrate never; hydrate on idle) { … } <!-- never exclude orice alt hydrate trigger -->| Trigger | Se declanșează | Parametri |
|---|---|---|
hydrate on idle | browserul e liber (requestIdleCallback) | opțional, un timeout: idle(500) |
hydrate on viewport | conținutul blocului intră în ecran | opțional, opțiuni IntersectionObserver literale; fără referință la alt element |
hydrate on interaction | click sau keydown în bloc | niciunul |
hydrate on hover | mouseover sau focusin în bloc | niciunul |
hydrate on immediate | imediat ce restul paginii a terminat de randat | — |
hydrate on timer(…) | după un interval | obligatoriu, în ms sau s |
hydrate when expr | expresia devine truthy | funcționează doar pe blocul deshidratat cel mai de sus |
hydrate never | niciodată: HTML static, cu tot subarborele | nu se combină cu alte trigger-e hydrate |
Mai multe trigger-e pe același bloc se combină cu „oricare” (OR). hydrate when are o limită: expresia aparține componentei părinte, care trebuie să fie ea însăși hidratată ca expresia să poată fi evaluată.
Blocuri imbricate
Arborele de componente e ierarhic: un copil nu poate fi activ fără părintele lui. Dacă trigger-ul se declanșează pe un bloc imbricat, Angular hidratează lanțul de sus în jos — întâi blocul deshidratat cel mai de sus, apoi pe rând până la cel care a cerut.
@defer (hydrate on interaction) {
<app-product-tabs /> <!-- 1. se hidratează întâi părintele… -->
@defer (hydrate on hover) {
<app-size-guide /> <!-- 2. …apoi copilul, chiar dacă hover-ul a fost pe el -->
}
}Hydrate triggers în CSR și la navigare
Hidratarea e o optimizare a încărcării inițiale de conținut randat pe server. Când utilizatorul ajunge la aceeași pagină prin navigare client-side (un routerLink), sau când aplicația nu are SSR deloc, trigger-ele hydrate sunt ignorate: blocul se comportă ca un @defer obișnuit, cu @placeholder și trigger-ele normale. Dacă nu ai pus niciun trigger normal, compilatorul adaugă on idle.
@defer (on interaction; hydrate on viewport) {
<app-reviews />
} @placeholder {
<button>Arată recenziile</button>
}
<!--
Încărcarea inițială, pagină randată pe server:
→ serverul randează <app-reviews /> (nu placeholder-ul)
→ în browser rămâne HTML static până intră în viewport (hydrate on viewport)
Navigare client-side (routerLink) sau aplicație fără SSR:
→ hydrate on viewport e ignorat
→ se vede @placeholder, blocul se încarcă la click (on interaction)
→ fără trigger normal, implicit ar fi fost on idle
-->@placeholderrămâne necesar — nu apare la încărcarea inițială cu SSR, dar apare la orice navigare client-side spre pagină.hydrate neverînseamnă „niciodată JS la încărcarea inițială”. La o randare client-side ulterioară, blocul își descarcă totuși dependențele, cu trigger-ul normal.- Într-o aplicație fără
provideClientHydration()(sau cuwithNoIncrementalHydration()), blocurile cu trigger-ehydrateproduc în dev mode avertismentul NG0508.
Erori de nepotrivire
Hidratarea pornește de la o promisiune: DOM-ul din browser e exact cel produs de server, inclusiv comentariile și spațiile. Orice lucru care încalcă promisiunea — parser-ul HTML al browserului, un script terț, un CDN, o ramură de template diferită — se termină într-una din trei situații: eroare, avertisment sau, cel mai perfid, nicio eroare dar o re-randare vizibilă.
| Cod | Înseamnă | Cauza tipică |
|---|---|---|
NG0500 | nodul găsit nu e cel așteptat (alt tag sau tip) ori lipsește | HTML invalid, manipulare directă a DOM-ului, script terț |
NG0501 | lipsesc noduri frate | noduri șterse din afara Angular |
NG0502 | nu găsește nodul la poziția așteptată | structură modificată după server |
NG0503 | noduri proiectate create în afara Angular | createComponent cu projectableNodes din DOM nativ |
NG0504 | ngSkipHydration pe un element care nu e gazdă de componentă | atributul pus pe un div |
NG0505 | (avertisment) lipsesc adnotările de pe server | provideClientHydration() lipsește din config-ul de server |
NG0506 | (avertisment) aplicația nu devine stabilă în 10 s | PendingTasks nefinalizate; cu Zone.js, timere în zona Angular |
NG0507 | HTML-ul a fost modificat după server | CDN care elimină comentariile |
<p class="promo"> <div>Livrare gratuită azi</div> </p>
<p class="promo"><div>Livrare gratuită azi</div></p><p class="promo"></p><div>Livrare gratuită azi</div><p></p>
NG0500: During hydration Angular expected <div> but the node was not found.
Angular expected this DOM:
<p class="promo">
<div>…</div> <-- AT THIS LOCATION
…
</p>
To fix this problem:
* check the "PromoBanner" component for hydration-related issues
* check to see if your template has valid HTML structure
…Ce se întâmplă: Serverul construiește DOM-ul prin API-uri, deci nesting-ul invalid supraviețuiește până la serializare. Parser-ul HTML din browser închide însă <p> când întâlnește <div>, iar Angular nu mai găsește nodul așteptat.
Fix: HTML valid: <p> acceptă doar conținut inline. Folosește un <div class="promo"> ca părinte sau un <span> în interior.
@if (isPlatformBrowser(…)) în template nu aruncă eroare, dar e una dintre cele mai scumpe greșeli: Angular aruncă DOM-ul serverului pentru bloc și îl reface, cu layout shift. Ține output-ul identic pe ambele platforme și mută inițializarea de browser în afterNextRender — sau folosește @defer, care randează pe server placeholder-ul. Variantele sunt pe pagina Cod compatibil cu serverul.
ngSkipHydration: ieșirea de urgență
Pentru o componentă care nu poate fi reparată imediat (tipic: o librărie care desenează direct în DOM, ca D3), atributul ngSkipHydration pe gazda ei o scoate din hidratare: componenta și copiii ei sunt distruși și re-randați, exact ca fără hidratare. Funcționează doar pe gazde de componente (altfel NG0504), iar pus pe componenta rădăcină dezactivează hidratarea pentru toată aplicația.
<!-- pe gazda componentei care manipulează DOM-ul direct (ex. un grafic D3) -->
<app-sales-chart ngSkipHydration [data]="sales()" />
<!-- ✗ NG0504: doar pe gazde de componente, nu pe elemente obișnuite -->
<div ngSkipHydration><app-sales-chart [data]="sales()" /></div>Debugging
- Consola, în dev mode: după ce aplicația devine stabilă apare un rezumat. Dacă nu-l vezi, hidratarea nu a pornit (verifică NG0505).
- Angular DevTools arată starea de hidratare a fiecărei componente și poate activa un overlay peste părțile hidratate; la o eroare de nepotrivire, evidențiază componenta vinovată.
- View source vs Elements: View source e HTML-ul de pe server, Elements e DOM-ul curent. Diferențele dintre ele (un
tbodyapărut, un banner inserat) sunt exact cauzele de NG0500. - Dezactivează JavaScript-ul în DevTools și reîncarcă: vezi exact ce primește utilizatorul înainte de hidratare.
- NG0506:
provideStabilityDebugging()listează task-urile care țin aplicația instabilă (cu Zone.js, importă șizone.js/plugins/task-tracking). preserveWhitespacestrebuie să aibă aceeași valoare întsconfig.app.jsonșitsconfig.server.json; implicitul (false) e varianta recomandată.
Angular hydrated 14 component(s) and 212 node(s), 0 component(s) were skipped.
5 defer block(s) were configured to use incremental hydration.
Learn more at https://angular.dev/guide/hydration.Când DA, când NU
- Hidratare: oricând ai SSR sau prerender. Fără ea, SSR-ul își pierde jumătate din sens (DOM-ul e refăcut).
- Event replay: implicit; contează mai ales pe pagini cu acțiuni rapide (coș, like, meniu) și rețele lente.
hydrate on viewportpentru tot ce e sub fold: recenzii, produse similare, footer bogat.hydrate on interaction/hoverpentru widget-uri grele care stau degeaba până la prima atingere: galerii, filtre, selectoare de mărime.hydrate neverpentru conținut cu adevărat static: articol, termeni, footer fără logică.
- Aplicații fără SSR: trigger-ele
hydratenu fac nimic. Folosește @defer cu trigger-e normale. @defer (hydrate …)pe componente de câțiva KB: chunk-ul în plus și request-ul costă mai mult decât economisești.hydrate neverpe ceva care are listeneri sau stare: nu vor funcționa niciodată la încărcarea inițială.ngSkipHydrationca „fix” permanent: repară cauza (HTML valid,afterNextRender, DOM manipulat prin Angular).- Ramuri
isPlatformBrowserîn template pentru a ascunde problemele: alternativa e@defersauafterNextRender.
Capcane frecvente
nativeElement.appendChild, innerHTML, document.querySelector(…).remove() în componente care rulează și pe server produc structuri pe care hidratarea nu le poate potrivi. Folosește template-ul (@if, @for, binding-uri, Renderer2) și amână codul strict de browser în afterNextRender.
- HTML invalid:
<div>în<p>,<a>în<a>, tabel fără<tbody>. Validează template-ul; browserul „repară” HTML-ul altfel decât l-a scris serverul. - Valori diferite pe server și în client (
Date.now(),Math.random(), ID-uri generate, fus orar): fără eroare, dar textul se schimbă sub ochii utilizatorului. Transferă valoarea cuTransferState. - Scripturi terțe (analytics, consent, chat) care modifică DOM-ul înainte de hidratare: încarcă-le după, cu
afterNextRender. withEventReplay()șiwithIncrementalHydration()lăsate „pentru siguranță” în v22: primul e redundant, al doilea e deprecated.provideClientHydration()simplu ajunge.@placeholderșters pe blocurile cuhydrate, „că oricum nu se vede”: la navigarea client-side se vede.- Date personale în transfer cache: răspunsurile ajung în HTML. Request-urile cu
Authorization/Cookiesunt excluse implicit — nu le include cuincludeRequestsWithAuthHeadersfără motiv; foloseștefiltersautransferCache: false. - CDN cu minificare HTML care șterge comentariile → NG0507 pe toate paginile.
Fișă rapidă
// ── Config (app.config.ts; ajunge și pe server prin mergeApplicationConfig)
provideClientHydration(); // v22: DOM + transfer cache + incremental + replay
provideClientHydration(withNoIncrementalHydration(), withEventReplay());
provideClientHydration(withI18nSupport());
provideClientHydration(withHttpTransferCacheOptions({ filter, includeHeaders, includePostRequests }));
provideClientHydration(withNoHttpTransferCache());
http.get(url, { transferCache: false }); // opt-out per request
// ── Template
// @defer (hydrate on idle | idle(ms) | viewport | viewport({ rootMargin }) | interaction | hover | immediate | timer(2s))
// @defer (hydrate when cond) @defer (hydrate never) @defer (on idle; hydrate on viewport)
// <app-chart ngSkipHydration /> host: { ngSkipHydration: 'true' } ← ultimă soluție
// ── Erori
// NG0500 nod diferit NG0501 lipsesc frați NG0502 lipsește nodul NG0503 noduri proiectate din afara Angular
// NG0504 ngSkipHydration pe non-componentă NG0505 lipsesc adnotările (provider lipsă pe server)
// NG0506 aplicația nu devine stabilă în 10 s NG0507 HTML modificat după server (comentarii șterse)
// NG0508 hydrate triggers fără hidratare incrementală activă