AngularAtlased. 22.2

Cap. 12SSR & hidratare54 / 75

SSR, prerender & render modes

Când randezi pe server, când la build și când doar în browser: RenderMode pe rută, app.routes.server.ts și costurile reale.

v22arhitectură ~0 min de citit Documentația oficială

Implicit, Angular livrează aplicația ca CSR (client-side rendering): serverul trimite un <app-root></app-root> gol, iar browserul trebuie să descarce, să parseze și să ruleze tot JavaScript-ul — apoi să mai ceară și datele — înainte să arate ceva util. SSR (randare pe server, la fiecare request) și SSG (prerender, la build) mută prima randare în altă parte: utilizatorul primește direct HTML cu conținut.

Cu @angular/ssr nu alegi o dată pentru toată aplicația, ci pe rută: fișierul app.routes.server.ts spune pentru fiecare path dacă e RenderMode.Prerender, RenderMode.Server sau RenderMode.Client. Asta se numește hybrid rendering. API-ul de rute de server a apărut în v19 și e stabil din v20.

Pe scurt
  • Randarea în browser (CSR), pe server (SSR) sau la build (prerender) se alege per rută, cu RenderMode în app.routes.server.ts.
  • Prerender pentru paginile care arată la fel pentru toți, SSR pentru cele personalizate sau cu date care se schimbă des, CSR pentru zonele de după login.
  • SSR îmbunătățește prima afișare și ce vede un crawler, dar adaugă un server de întreținut și cod care trebuie să meargă și pe server.
  • Rutele cu parametri se pot prerandarea dacă le dai lista de valori, cu getPrerenderParams.
  • Cache-ul de la CDN contează adesea mai mult decât viteza serverului.
De ce contează

SSR nu e un comutator gratuit de performanță. Câștigi LCP, SEO și preview-uri corecte pe rețelele sociale; plătești cu un server Node de operat, cu cod care trebuie să ruleze și fără window și cu o fereastră în care pagina se vede, dar încă nu răspunde la click. Decizia bună se ia per rută, nu per proiect.

Trei moduri de randare

CSR · ClientSSR · ServerSSG · Prerender
HTML-ul apareîn browser, după JSpe server, la fiecare requestla build, o singură dată
Serverul, per requestservește un fișier static (shell-ul gol)pornește aplicația, cere datele, randeazăservește un fișier static
TTFBmicinclude randarea și API-urile chemate de eaminim, mai ales de pe CDN
LCPdupă JS + bootstrap + request APIimediat ce sosește HTML-ulimediat ce sosește HTML-ul
SEO, preview-urislab: depinde de JSexcelentexcelent
Conținut per utilizatordadanu — HTML identic pentru toți
Hostingorice hosting static / CDNruntime Node (sau alt runtime JS)orice hosting static / CDN
Cost pe cerere≈ 0CPU + memorie pentru o randare completă≈ 0

Ce face concret serverul în modul Server: pentru fiecare request, @angular/ssr creează o platformă nouă, apelează funcția de bootstrap din main.server.ts, lasă routerul să navigheze la URL, așteaptă ca aplicația să devină stabilă (toate request-urile HTTP și PendingTasks terminate), serializează DOM-ul în HTML și distruge aplicația. Consecința practică: TTFB-ul unei pagini SSR e cel puțin cât cel mai lent API chemat în timpul randării.

Într-o aplicație hibridă reală, modurile conviețuiesc: homepage-ul și paginile de marketing sunt prerandate, căutarea e randată pe server, iar zona de cont rulează doar în browser.

Ce se întâmplă la un request

Cinci momente contează, în ordinea în care le trăiește utilizatorul:

  • TTFB (time to first byte) — primul octet al răspunsului HTML.
  • FCP (first contentful paint) — primul text sau prima imagine pe ecran.
  • LCP (largest contentful paint) — apare elementul principal (titlul, imaginea produsului). E metrica pe care o simte utilizatorul ca „s-a încărcat”.
  • Hidratarea — JS-ul a sosit, Angular preia HTML-ul existent și atașează listenerii.
  • Interactiv — un click chiar face ceva. Din acest moment contează INP (cât de repede răspunde pagina).

La SSR și SSG, între LCP și „interactiv” există o fereastră în care pagina arată gata, dar butoanele nu au încă listener. Pe un telefon modest, cu un bundle de câteva sute de KB, fereastra are ușor 1–3 secunde. Rulează simularea și compară cele trei moduri pe aceeași rețea.

LiveTimeline-ul unui request (simulare)
magazin.ro/produse/42
Magazin
Căști wireless Aria X2
349 lei · livrare mâine
interactiv
0 ms500 ms1,00 s1,50 s2,00 s
Server: API + render
HTML · 40 KB
main.js · 260 KB
Hidratare
TTFB 260 msFCP = LCP 535 msinteractiv 2,36 sneinteractiv 1,82 s

SSR: TTFB include munca serverului (care așteaptă API-ul), apoi conținutul apare imediat. Banda galbenă = conținut vizibil, dar fără listeneri până la hidratare.

Alege modul și rețeaua, apoi trage de cursor. Banda galbenă e fereastra „vizibil, dar neinteractiv”; apasă „Adaugă în coș” în timpul ei. Cifrele sunt ilustrative, nu măsurători.

Observă ce nu se schimbă: main.js are aceeași dimensiune în toate cele trei moduri. SSR nu micșorează JavaScript-ul, deci interactivitatea vine cam în același timp ca la CSR — uneori chiar puțin mai târziu, pentru că HTML-ul e mai mare și hidratarea costă și ea CPU. Câștigul real e LCP-ul. Ca să micșorezi fereastra neinteractivă: mai puțin JS inițial (rute lazy, @defer), hidratare incrementală și event replay, care nu pierde click-urile din fereastră.

Ce vede un crawler

Un crawler pornește de la HTML-ul primit (ce vezi cu View source, nu în tab-ul Elements). La CSR, acel HTML e identic pentru orice URL. Googlebot poate rula JavaScript, dar într-o etapă ulterioară și cu limite; bot-urile care generează preview-uri în chat-uri și rețele sociale citesc de regulă doar HTML-ul inițial. Mai e un detaliu ignorat des: status code-ul. Un server static nu știe că /produse/9999 nu există, așa că răspunde 200 — un „soft 404”.

LiveCe primește un crawler: CSR vs SSR vs SSG (simulare)
curl https://magazin.ro/produse/42
HTTP/1.1 200 OK<!doctype html><html lang="ro"><head>  <title>Magazin</title>  <link rel="stylesheet" href="styles-5INURTSO.css"></head><body>  <app-root></app-root>  <script src="main-HG7KQWZB.js" type="module"></script></body></html>
Preview într-un chat (bot care citește doar HTML-ul)
fără imagine
magazin.ro
Magazin
— fără descriere —
  • ✓Status HTTP corect200
  • ✕<title> specific paginiiMagazin
  • ✕meta description / og:*lipsă
  • ✕Text vizibil în HTML0 cuvinte
  • ✕Link-uri <a href> de urmat0

Același HTML pentru orice URL: tot conținutul apare abia după ce rulează JavaScript-ul. Bot-urile de preview nu-l rulează; Googlebot îl rulează, dar într-o etapă ulterioară și cu limite.

Comută modul și bifează „produs inexistent”. Urmărește status-ul, title, meta-urile și marcajele adăugate de Angular (ngh, jsaction, ng-state).

Title și meta tag-urile se setează cu serviciile Title și Meta din @angular/platform-browser. Ele lucrează prin DOCUMENT, deci funcționează și pe server: ce setezi în timpul randării ajunge în HTML-ul trimis. Pentru pagini fără date ajunge proprietatea title a rutei (sau un TitleStrategy propriu, dacă vrei un format comun).

import { Component, computed, effect, inject, input } from '@angular/core';
import { httpResource } from '@angular/common/http';
import { Meta, Title } from '@angular/platform-browser';

interface Product { id: string; name: string; description: string; imageUrl: string }

@Component({
  selector: 'app-product-page',
  template: `
    @if (product.hasValue()) {
      <h1>{{ product.value().name }}</h1>
      <p>{{ product.value().description }}</p>
    }
  `,
})
export default class ProductPage {
  readonly id = input.required<string>(); // din ruta produse/:id (withComponentInputBinding)

  private readonly title = inject(Title);
  private readonly meta = inject(Meta);
  protected readonly product = httpResource<Product>(() => `/api/products/${this.id()}`);

  constructor() {
    // Rulează și pe server: tag-urile ajung în HTML-ul trimis crawler-elor.
    effect(() => {
      if (!this.product.hasValue()) return;
      const p = this.product.value();
      this.title.setTitle(`${p.name} — Magazin`);
      this.meta.updateTag({ name: 'description', content: p.description });
      this.meta.updateTag({ property: 'og:title', content: p.name });
      this.meta.updateTag({ property: 'og:description', content: p.description });
      this.meta.updateTag({ property: 'og:image', content: p.imageUrl });
    });
  }
}
Un singur loc pentru SEO

Pune logica într-un serviciu SeoService.update({ title, description, image, canonical }) și apelează-l din paginile de detaliu. Pentru <link rel="canonical"> ai nevoie de DOCUMENT — detalii în Cod compatibil cu serverul.

Setup: ce generează ng add

# proiect nou, cu SSR + prerender configurate de la început
ng new magazin --ssr

# proiect existent: adaugă @angular/ssr, express și fișierele de server
ng add @angular/ssr

# build de producție → dist/magazin/browser + dist/magazin/server
ng build

# pornește serverul Node (script adăugat de ng add în package.json)
npm run serve:ssr:magazin        # = node dist/magazin/server/server.mjs

Comanda adaugă dependențele (@angular/ssr, express), patru fișiere și câteva opțiuni în angular.json. Merită să știi ce face fiecare, pentru că pe ele le vei modifica:

  • server.ts — serverul Express. Servește fișierele statice din dist/…/browser, iar restul request-urilor le dă lui AngularNodeAppEngine.handle(), care întoarce un Response (sau null dacă URL-ul nu e o rută Angular). Exportul reqHandler e folosit de CLI: ng serve randează și el pe server.
  • main.server.ts — funcția de bootstrap apelată de server la fiecare randare.
  • app.config.server.ts — provideServerRendering(withRoutes(serverRoutes)), combinat cu config-ul de browser prin mergeApplicationConfig.
  • app.routes.server.ts — modul de randare pe rută. Implicit, totul e Prerender.
  • În app.config.ts apare provideClientHydration(), ca browserul să refolosească HTML-ul în loc să-l refacă.
import {
  AngularNodeAppEngine,
  createNodeRequestHandler,
  isMainModule,
  writeResponseToNodeResponse,
} from '@angular/ssr/node';
import express from 'express';
import { join } from 'node:path';

const browserDistFolder = join(import.meta.dirname, '../browser');

const app = express();
const angularApp = new AngularNodeAppEngine(); // o singură instanță pentru tot serverul

// 1. (opțional) API-urile tale: app.get('/api/{*splat}', (req, res) => { ... });

// 2. Fișierele statice (JS/CSS/imagini au hash în nume → cache de un an)
app.use(express.static(browserDistFolder, { maxAge: '1y', index: false, redirect: false }));

// 3. Orice altceva: Angular decide — HTML prerandat, SSR sau shell-ul CSR
app.use((req, res, next) => {
  angularApp
    .handle(req)
    .then((response) => (response ? writeResponseToNodeResponse(response, res) : next()))
    .catch(next);
});

if (isMainModule(import.meta.url) || process.env['pm_id']) {
  const port = process.env['PORT'] || 4000;
  app.listen(port, (error) => {
    if (error) throw error;
    console.log(`Node Express server listening on http://localhost:${port}`);
  });
}

// folosit de ng serve / ng build (și de unele platforme serverless)
export const reqHandler = createNodeRequestHandler(app);

Detaliu din server.ts: express.static are index: false, deci nici măcar / nu e servit direct de pe disc. Orice pagină trece prin Angular, care aplică render mode-ul, headerele și status-ul configurate pe rută.

allowedHosts în producție (v22)

Pentru a preveni SSRF, serverul validează header-ul Host. Din v22, un host necunoscut primește 400 Bad Request (înainte, serverul cădea pe CSR). ng add pune "allowedHosts": []: completează domeniile tale în angular.json (sunt acceptate și wildcard-uri ca *.magazin.ro), în opțiunile AngularNodeAppEngine sau în variabila de mediu NG_ALLOWED_HOSTS. În spatele unui load balancer, headerele X-Forwarded-* sunt ignorate până nu le permiți explicit cu trustProxyHeaders. Detalii în Securitate.

Dacă ai un proiect mai vechi al cărui server.ts folosește CommonEngine: API-ul e deprecated din v22, înlocuitorul fiind AngularNodeAppEngine (Node) sau AngularAppEngine (runtime-uri bazate pe Web API Request/Response).

RenderMode pe rută

import { RenderMode, ServerRoute } from '@angular/ssr';

export const serverRoutes: ServerRoute[] = [
  // Identic pentru toți, se schimbă doar la deploy → HTML generat la build (SSG)
  { path: '', renderMode: RenderMode.Prerender },
  { path: 'despre', renderMode: RenderMode.Prerender },

  // Public, indexat, date care se schimbă des → randat la fiecare request (SSR)
  { path: 'cautare', renderMode: RenderMode.Server },

  // În spatele login-ului: fără SEO, HTML personal → doar în browser (CSR)
  { path: 'cont/**', renderMode: RenderMode.Client },

  // Plasa de siguranță pentru orice rută Angular nelistată mai sus
  { path: '**', renderMode: RenderMode.Server },
];

Regulile care contează în practică:

  • Potrivirea: un segment exact bate un parametru (:id), care bate **. Deci ** e plasa de siguranță, nu o regulă care „suprascrie”.
  • Două direcții de validare: fiecare rută din router trebuie să se potrivească cu o rută de server, iar fiecare rută de server (în afară de cele terminate în /**) trebuie să existe în router. Altfel build-ul se oprește cu un mesaj explicit.
  • Path-ul nu începe cu „/” — '/despre' e eroare la build.
  • Ce primește browserul: Prerender → fișierul generat la build (cu ETag); Server → HTML randat acum; Client → index.csr.html, shell-ul gol, servit tot de serverul tău.
LiveCe render mode pentru ruta asta? (simulare)
Exemple:
RecomandareRenderMode.PrerenderSSG la build + SSR pentru restul
  • HTML-ul e identic pentru toți și stabil: îl generezi o dată, la build.
  • Servit ca fișier static (ideal de pe CDN): TTFB minim, cost aproape zero per request.
Atenție la
  • Datele se schimbă zilnic → ai nevoie de un build programat (ex. nightly în CI) care re-generează paginile.
  • Mii de pagini adaugă minute la build; măsoară timpul și dimensiunea deploy-ului.
import { PrerenderFallback, RenderMode, ServerRoute } from '@angular/ssr';

export const serverRoutes: ServerRoute[] = [
  {
    path: 'produse/:id',
    renderMode: RenderMode.Prerender,
    async getPrerenderParams() {
      const api = inject(CatalogApi); // sincron, înainte de await
      const items = await api.mostVisited(500);
      return items.map((x) => ({ id: String(x.id) }));
    },
    fallback: PrerenderFallback.Server,
  },
  // ...
];
Încearcă exemplele sau descrie o rută proprie. Recomandarea și intrarea din app.routes.server.ts se actualizează live; „Atenție la” arată costul ascuns al alegerii.

Headere, status și redirect-uri

Fiecare ServerRoute poate avea headers și status. Excepția: rutele Prerender acceptă headers, dar nu status — tipul ServerRoutePrerender îl exclude, pentru că fișierul e generat o singură dată. Headerele unei pagini prerandate se aplică doar când ea e servită prin serverul Node, nu de pe un hosting static.

import { RenderMode, ServerRoute } from '@angular/ssr';

export const serverRoutes: ServerRoute[] = [
  {
    // HTML public: CDN-ul îl poate ține 60 s și servi versiunea veche cât reîmprospătează
    path: 'cautare',
    renderMode: RenderMode.Server,
    headers: { 'Cache-Control': 'public, s-maxage=60, stale-while-revalidate=300' },
  },
  {
    // redirect permanent: { path: 'oferte', redirectTo: 'promotii' } în app.routes.ts
    path: 'oferte',
    renderMode: RenderMode.Server,
    status: 301, // fără status: 302
  },
  {
    // prerender: headers da, status NU (tipul ServerRoutePrerender îl exclude)
    path: 'despre',
    renderMode: RenderMode.Prerender,
    headers: { 'X-Frame-Options': 'DENY' },
  },
  { path: '**', renderMode: RenderMode.Server },
];

Un redirectTo din router devine pe server un redirect HTTP real (302 implicit; poți pune status: 301, 303, 307 sau 308), răspuns fără să mai randeze aplicația. La fel, dacă un guard redirecționează în timpul randării (vezi Guards & resolvers), URL-ul final diferă de cel cerut și serverul răspunde cu redirect. Cu outputMode: 'static', unde nu există server, redirect-urile sunt prerandate ca „soft redirects” cu <meta http-equiv="refresh"> în HTML.

Status și headere decise la runtime

Când status-ul depinde de date (produsul nu există), îl setezi din componentă prin RESPONSE_INIT. Pentru cookie-uri, limbă sau user-agent citești request-ul prin REQUEST. Ambele vin din @angular/core și sunt null în browser, la prerender și la build — tratează mereu cazul null.

import { Component, RESPONSE_INIT, inject } from '@angular/core';

@Component({
  selector: 'app-not-found',
  template: `<h1>Pagina nu există</h1>`,
})
export default class NotFound {
  constructor() {
    // RESPONSE_INIT e null în browser, la prerender și la build → verifică mereu.
    const response = inject(RESPONSE_INIT);
    if (response) response.status = 404; // crawler-ul vede 404, nu „200 cu text de eroare”
  }
}

Prerender cu parametri

Rutele fără parametri sunt descoperite și prerandate automat. Pentru produse/:id, Angular nu poate ghici ce ID-uri există, așa că îi dai o funcție getPrerenderParams. Ea rulează la build, într-un injection context (poți folosi inject()), și întoarce o listă de obiecte parametru → valoare. O rută Prerender cu parametri, dar fără getPrerenderParams, oprește build-ul.

import { inject } from '@angular/core';
import { PrerenderFallback, RenderMode, ServerRoute } from '@angular/ssr';
import { ProductApi } from './products/product-api';

export const serverRoutes: ServerRoute[] = [
  {
    path: 'produse/:id',
    renderMode: RenderMode.Prerender,
    async getPrerenderParams() {
      const api = inject(ProductApi); // ✓ inject() sincron, ÎNAINTE de orice await
      const ids = await api.topProductIds(500); // rulează la BUILD, nu la request
      return ids.map((id) => ({ id: String(id) })); // valorile trebuie să fie string-uri
    },
    // /produse/9999 nu a fost prerandat → ce facem la cerere?
    fallback: PrerenderFallback.Server, // implicit: SSR la cerere
  },
  {
    path: 'blog/:slug/**', // parametri + catch-all
    renderMode: RenderMode.Prerender,
    async getPrerenderParams() {
      return [{ slug: 'ssr', '**': 'partea/1' }]; // → /blog/ssr/partea/1
    },
  },
  { path: '**', renderMode: RenderMode.Server },
];

Ce se întâmplă cu un URL care nu a fost generat la build depinde de fallback:

PrerenderFallbackPentru /produse/9999 (neprerandat)Când îl alegi
Server (implicit)randat la cerere pe servercatalog mare: prerandezi top N, restul la cerere
Clientprimește shell-ul CSR, randat în browservrei SEO doar pentru paginile generate și nu vrei SSR
NoneAngular nu răspunde (request-ul merge mai departe, de obicei 404)lista de la build e completă prin definiție
inject() după await

inject() funcționează doar sincron, la începutul funcției. Un inject() scris după await nu mai e în injection context și aruncă eroare. Ia toate dependențele în primele rânduri, apoi fă request-urile.

Costul e la build: fiecare URL e o randare completă. Zece mii de produse înseamnă zece mii de randări la fiecare deploy și zece mii de fișiere de încărcat. Dacă datele se schimbă zilnic, ai nevoie și de un build programat care să le regenereze.

Deploy, cache și costuri

outputMode din angular.json decide ce produce build-ul. Cu "server" (ce pune ng add) primești dist/app/browser plus dist/app/server/server.mjs și ai nevoie de un proces Node. Cu "static" primești doar fișiere: pagini prerandate și shell-ul CSR, deployabile pe orice CDN. O rută RenderMode.Server în modul static e eroare la build.

{
  "projects": {
    "magazin": {
      "architect": {
        "build": {
          "options": {
            "outputMode": "static"
          }
        }
      }
    }
  }
}

Straturile de cache

CeCumAtenție
JS, CSS, imagini cu hashmaxAge: '1y' în express.static / CDNnumele se schimbă la fiecare build, deci cache-ul lung e sigur
HTML prerandatfișier static, ETag → 304 Not Modifiedse schimbă doar la deploy
HTML SSR publicheaders pe rută: public, s-maxage=60, stale-while-revalidate=300doar pentru conținut identic pentru toți
HTML SSR personalprivate, no-storeniciodată public: CDN-ul l-ar servi altui utilizator
Date HTTP din randaretransfer cache: răspunsurile GET/HEAD ajung în HTML și sunt refolosite la hidratarerequest-urile cu Authorization/Cookie sunt excluse implicit

Transfer cache-ul e motivul pentru care, în timeline, SSR nu mai face request-ul la API din browser: datele aduse de HttpClient pe server sunt serializate în <script id="ng-state"> și citite la pornire. Opțiunile lui (withHttpTransferCacheOptions, withNoHttpTransferCache) sunt pe pagina Hidratare. Tot aici: pe server, cu backend-ul fetch implicit, fiecare răspuns HTTP e limitat la 1 MB (eroarea NG02825); limita se schimbă cu provideServerRendering({ maxResponseBodySize }, …).

Costul operațional

  • CPU per request: fiecare randare SSR e un bootstrap complet. Fără cache la CDN, traficul de la bot-uri se plătește în servere.
  • TTFB legat de backend: serverul așteaptă stabilitatea, deci un API lent îți încetinește toate paginile. Pune timeout-uri și mută datele secundare în @defer (pe server, un bloc fără trigger hydrate randează doar placeholder-ul).
  • Memorie partajată între request-uri: codul e evaluat o singură dată per proces, deci variabilele de modul și provider-ii useValue sunt comuni tuturor utilizatorilor.
  • Încă un sistem în producție: loguri, monitorizare, scalare, versiune de Node, cold start pe serverless.
import { InjectionToken } from '@angular/core';

// ✗ Stare la nivel de modul: pe server, modulul e evaluat O SINGURĂ DATĂ,
//    deci variabila e comună tuturor request-urilor (și tuturor utilizatorilor).
let currentUser: string | null = null;

// ✗ useValue la nivel de aplicație: aceeași valoare până la restartul serverului.
export const RENDERED_AT = new InjectionToken<number>('RENDERED_AT');
const bad = { provide: RENDERED_AT, useValue: Date.now() };

// ✓ useFactory: rulează la fiecare randare (fiecare request are injectorul lui).
const good = { provide: RENDERED_AT, useFactory: () => Date.now() };

// ✓ Starea per utilizator stă în servicii (instanțiate per request), nu în variabile globale.

Când DA, când NU

SSR / SSG merită
  • Pagini publice care trebuie indexate și distribuite: produse, articole, landing pages. Prerender dacă le poți genera la build, Server dacă nu.
  • LCP-ul se traduce în bani (e-commerce, media), iar o parte din utilizatori au telefoane și rețele modeste.
  • Conținut stabil (documentație, blog, marketing): Prerender + outputMode: 'static', fără niciun server de întreținut.
  • Conținut public cu date proaspete și trafic mare: Server + cache scurt la CDN.
  • Ai nevoie de status code-uri corecte (404, 301) pentru SEO.
Rămâi la CSR (sau alege altceva)
  • Aplicații în spatele login-ului (dashboard, admin, back-office): Client. Investește în bundle mic, rute lazy și @defer.
  • Nu ai cine să opereze un server Node: rămâi la CSR sau la Prerender cu outputMode: 'static'.
  • „Aplicația e lentă după încărcare”: SSR nu ajută după hidratare. Problema e în JS sau în change detection → Performanță, Change detection.
  • Rute construite pe API-uri de browser (hărți, canvas, editoare): RenderMode.Client pe ruta respectivă.
  • HTML personalizat cu trafic mare: nu randa per utilizator. Prerandează partea comună și încarcă bucățile personale în browser.

Capcane frecvente

window is not defined

Cod care atinge window, document sau localStorage în constructor, în ngOnInit sau la nivel de modul crapă pe server. Soluția: afterNextRender, token-ul DOCUMENT și implementări de serviciu separate pentru server și browser — toate pe pagina Cod compatibil cu serverul.

Date amestecate între utilizatori

O variabilă let currentUser la nivel de modul sau un useValue calculat la pornire trăiesc cât procesul Node, nu cât request-ul. Pune starea în servicii (instanțiate per randare) și folosește useFactory pentru valori calculate per request.

  • Cache public pe HTML personalizat → un utilizator vede pagina altuia. Pe rutele Server cu date personale pune Cache-Control: private, no-store.
  • allowedHosts gol în producție → 400 pe toate paginile (v22). Configurează domeniile sau NG_ALLOWED_HOSTS.
  • „Soft 404”: componenta „not found” răspunde 200. Setează status = 404 prin RESPONSE_INIT.
  • Fallback SPA greșit pe hosting static: cu prerender, index.html e pagina rutei /. Rutele Client trebuie să cadă pe index.csr.html.
  • Aplicația nu devine stabilă: un PendingTask fără cleanup (sau, cu Zone.js, un setInterval pornit în zona Angular) ține randarea pe loc, iar request-ul nu se mai termină.
  • CDN care minifică HTML-ul și șterge comentariile → hidratarea eșuează cu NG0507. Comentariile adăugate de Angular sunt necesare; dezactivează opțiunea pe CDN.
  • „Am pus SSR și TTI a rămas la fel” — e normal: JS-ul e același. Măsoară LCP, nu TTI, și redu JS-ul inițial separat.

Fișă rapidă

// ng new app --ssr  |  ng add @angular/ssr  |  ng build  |  node dist/app/server/server.mjs

// app.config.server.ts
provideServerRendering(withRoutes(serverRoutes));
provideServerRendering({ maxResponseBodySize: 5 * 1024 * 1024 }, withRoutes(serverRoutes)); // implicit 1 MB

// app.config.ts (browser)
provideClientHydration();

// app.routes.server.ts — import { RenderMode, PrerenderFallback, ServerRoute } from '@angular/ssr'
export const serverRoutes: ServerRoute[] = [
  { path: 'despre', renderMode: RenderMode.Prerender }, // HTML la build (SSG)
  {
    path: 'produse/:id',
    renderMode: RenderMode.Prerender,
    getPrerenderParams: async () => [{ id: '1' }, { id: '2' }], // rulează la build
    fallback: PrerenderFallback.Server, // | Client | None — implicit Server
  },
  { path: 'cautare', renderMode: RenderMode.Server, headers: { 'Cache-Control': 'no-store' } },
  { path: 'cont/**', renderMode: RenderMode.Client }, // shell gol, randare în browser
  { path: '**', renderMode: RenderMode.Server }, // plasa de siguranță
];

// @angular/core — toate sunt null în browser, la prerender și la build
inject(REQUEST); // Request: headere, cookie-uri, URL
inject(RESPONSE_INIT); // ResponseInit: status / headere setate dinamic
inject(REQUEST_CONTEXT); // al doilea argument din angularApp.handle(req, context)

// angular.json → architect.build.options
//   "outputMode": "server" | "static"               static = fără Node: doar Prerender + Client
//   "security": { "allowedHosts": ["exemplu.ro"] }  host necunoscut → 400 Bad Request (v22)

Cap. 12 · 0 din 3 terminate

Ai terminat lecția?

Marcheaz-o și apare pe harta ta: în cuprins și pe prima pagină.

Se marchează singură după ce citești toată lecția.

  1. SSR, prerender & render modes
  2. Hidratare: event replay & incremental
  3. Cod compatibil cu serverul
Vezi toată harta ta

Toate cele 75 de pagini · scrie ca să filtrezi

  • 01Ce e nou în Angular 21 & 22Toate schimbările importante din v21 și v22, pe scurt, cu link către pagina care le explică.Start
  • 01Prima ta aplicație, pas cu pasO listă de lecturi construită de la o componentă goală la o aplicație terminată — signals, @for, input-uri și output-uri, un serviciu și un formular, pas cu pas.Start
  • 01Decizii rapide„Unde pun asta?”, „Signal sau Observable?”, „Shared sau feature?” — răspunsuri scurte la întrebările de zi cu zi.Start
  • 01Întrebări de interviu Angular60 de întrebări reale de interviu pentru junior, mid și senior, cu răspunsul de dat, întrebarea care urmează și greșelile de evitat. Plus o simulare de interviu.Start
  • 02Standalone & bootstrapCum pornește o aplicație fără NgModule: bootstrapApplication, app.config.ts, imports în componentă.Fundamente moderne
  • 02Data binding și evenimenteInterpolare, binding pe proprietăți și pe atribute, evenimente cu $event și filtre de taste și cum e construit [( )] — ce intră în DOM și ce se întoarce.Fundamente moderne
  • 02Control flow: @if, @for, @switchSintaxa de blocuri care înlocuiește *ngIf/*ngFor/ngSwitch, cu track, @empty și verificare exhaustivă.Fundamente moderne
  • 02@let și expresiile noi din templateVariabile locale cu @let, arrow functions, spread, regex, instanceof, comentarii în tag-uri.Fundamente moderne
  • 02@defer — lazy loading în templateÎncarci componente grele doar când e nevoie: triggere, prefetch, placeholder, loading, error.Fundamente moderne
  • 02Class & style bindingsDe ce [class.x] și [style.x] au înlocuit ngClass și ngStyle (cu migrare automată din v21).Fundamente moderne
  • 02Stilurile componentelor și încapsularea:host, cele trei moduri de încapsulare, teme cu variabile CSS și de ce ::ng-deep e ultima soluție.Fundamente moderne
  • 02Pipes: când și cumPipe pur vs impur, pipe custom și cum alegi între pipe, computed() și o metodă.Fundamente moderne
  • 03signal() și computed()Primitivele reactive: stare scriibilă, stare derivată, egalitate, untracked și reguli de imutabilitate.Signals & reactivitate
  • 03effect() — când DA, când NUEfecte secundare sincronizate cu semnale: localStorage, logging, API-uri imperative. Și capcanele lor.Signals & reactivitate
  • 03linkedSignal() — stare derivată editabilăUn semnal care se resetează când se schimbă sursa, dar pe care îl poți și seta manual.Signals & reactivitate
  • 03resource, rxResource, httpResourceDate asincrone ca semnale: status, value, error, reload, anulare automată. Stabile din v22.Signals & reactivitate
  • 03debounced() — semnale cu întârziereVarianta „debounce” a unui semnal, fără RxJS. Perfect pentru căutare.Signals & reactivitate
  • 03Liste: căutare, filtrare, sortare, paginareUn lanț de computed-uri care recalculează doar ce s-a schimbat, cu filtrele ținute în URL, ca un link să redeschidă aceeași vedere.Signals & reactivitate
  • 03Observables de la zeroCe e un Observable, subscribe și unsubscribe, operatorii pe care îi folosești în fiecare săptămână, cold vs hot — și când ajunge un signal.Signals & reactivitate
  • 03Signals ↔ RxJStoSignal, toObservable, takeUntilDestroyed, outputFromObservable — și când RxJS rămâne alegerea bună.Signals & reactivitate
  • 04input() și output()Inputuri ca semnale (required, transform, alias) și outputuri tipizate. Data down, events up.Componente: comunicare
  • 04model() — two-way bindingBanana-in-a-box [( )] cu un singur API. Când îl folosești și când e mai bine input + output.Componente: comunicare
  • 04viewChild & contentChildInterogări ca semnale: viewChild, viewChildren, contentChild, contentChildren, opțiunea read.Componente: comunicare
  • 04Lifecycle & DestroyRefCe hook-uri mai contează în era signals, afterNextRender, DestroyRef și ngOnChanges tipizat.Componente: comunicare
  • 04Content projectionng-content cu select, sloturi multiple, conținut implicit și capcana proiecției condiționale.Componente: comunicare
  • 04ng-template & ngTemplateOutletTemplate-uri ca valori: render props în Angular, context tipizat și personalizare de componente.Componente: comunicare
  • 05Smart vs DumbContainer vs presentational: cine ține starea, cine doar afișează, și cum le împarți corect.Design de componente
  • 05Componente generice <T>Un DataTable<T> sau Select<T> tipizat corect, cu template-uri pentru celule.Design de componente
  • 05Componente dinamiceNgComponentOutlet, createComponent cu inputBinding/outputBinding și un dashboard construit din config.Design de componente
  • 05Directive & host compositionDirective atribut, metadata host: {} și Directive Composition API (hostDirectives).Design de componente
  • 05Controale custom de formularFormValueControl (Signal Forms) vs ControlValueAccessor: un rating cu stele, în ambele variante.Design de componente
  • 05Change detectionOnPush e implicit din v22, zone.js lipsește din v21. Ce declanșează randarea și cum să nu te arzi.Design de componente
  • 06inject() & @Service()Noul decorator @Service() din v22, inject() vs constructor și contextul de injecție.Dependency Injection
  • 06Providers & ierarhia de injectoriRoot vs rută vs componentă, useClass/useValue/useFactory/useExisting și modificatorii optional/self/skipSelf.Dependency Injection
  • 06InjectionToken & provideX()Configurare tipizată, funcții provideFeature(), makeEnvironmentProviders și inițializatori.Dependency Injection
  • 06injectAsync() — servicii lazyÎncarci un serviciu greu abia când ai nevoie de el, cu prefetch la idle.Dependency Injection
  • 07Structura de folderecore / shared / features, ce intră în fiecare și cum arată un feature pe dinăuntru.Arhitectura aplicației
  • 07Shared vs featureCând muți o componentă în shared și (mai ales) când NU. Checklist concret.Arhitectura aplicației
  • 07Interfețe & modeleInterfață inline sau fișier separat? DTO vs model de domeniu, type vs interface, unde le pui.Arhitectura aplicației
  • 07State management cu signalsStore-uri simple pe semnale, facade, stare locală vs globală și când îți trebuie NgRx.Arhitectura aplicației
  • 07Style guide & namingConvențiile oficiale actuale: nume de fișiere fără sufixe, protected, readonly, ordinea membrilor.Arhitectura aplicației
  • 07Barrels, aliasuri & granițeindex.ts, path aliases, API public pe feature și reguli de dependență care se pot verifica automat.Arhitectura aplicației
  • 07Erori: @boundary & ErrorHandlerNoul bloc @boundary din v22.2, ErrorHandler global, erori HTTP și erori din resource.Arhitectura aplicației
  • 08Routing modern & lazy loadingloadComponent, rute copil, parametri ca input-uri și moștenirea parametrilor (default nou în v22).Routing
  • 08Guards & resolverscanActivate, canMatch, canDeactivate și resolve ca funcții, cu redirect prin UrlTree / RedirectCommand.Routing
  • 09Signal Forms — bazeleFormulare construite pe semnale: form(), [formField], validare prin schemă. Stabile din v22.Formulare
  • 09Signal Forms — avansatValidare custom și cross-field, async/HTTP cu debounce, liste dinamice, disabled/hidden, scheme reutilizabile.Formulare
  • 09Reactive Forms tipizateFormGroup tipizat, nonNullable, FormArray — și cum conviețuiesc cu Signal Forms.Formulare
  • 10HttpClient, interceptors & API layerHttpClient fără setup (v21), Fetch implicit (v22), interceptori funcționali și stratul de API cu mapare DTO.HTTP & date
  • 11Performanță — checklistCe chiar contează: OnPush + signals, track, @defer, lazy routes, imagini, bundle și hidratare.Calitate & performanță
  • 11Testare cu VitestVitest e runner-ul implicit din v21. TestBed, componente cu semnale, servicii, HTTP și harness-uri.Calitate & performanță
  • 11DevTools: cum te uiți în aplicațieChrome DevTools și Angular DevTools pas cu pas: ce arată fiecare panou, cu exerciții care merg chiar pe site-ul ăsta.Calitate & performanță
  • 11Angular AriaComponente headless, accesibile, pe care le stilizezi cum vrei: tabs, accordion, menu, listbox...Calitate & performanță
  • 12SSR, prerender & render modesCând randezi pe server, când la build și când doar în browser: RenderMode pe rută, app.routes.server.ts și costurile reale.SSR & hidratare
  • 12Hidratare: event replay & incrementalCum preia browserul HTML-ul de pe server fără să-l refacă: hidratare completă, event replay, @defer (hydrate on …) și erorile de nepotrivire.SSR & hidratare
  • 12Cod compatibil cu serverulwindow nu există pe server: PLATFORM_ID, afterNextRender, DOCUMENT, TransferState, cache-ul HTTP și PendingTasks.SSR & hidratare
  • 13Micro frontendsCând merită să spargi o aplicație în mai multe deploy-uri și când e doar complexitate: Native Federation, Module Federation, iframes, Web Components.Arhitectură la scară
  • 13Web Components cu Angular ElementscreateCustomElement: o componentă Angular folosită ca tag HTML oriunde, plus comunicarea între aplicații prin evenimente și BroadcastChannel.Arhitectură la scară
  • 13Monorepo, librării & granițeUn workspace cu mai multe aplicații și librării: ng generate library, Nx, entry points secundare, publicare și reguli de dependență.Arhitectură la scară
  • 13Arhitectură hexagonală (ports & adapters)Domeniul fără Angular, porturi ca clase abstracte și adaptoare interschimbabile: HTTP, localStorage sau in-memory, fără să atingi UI-ul.Arhitectură la scară
  • 14Semnalele pe dinăuntruGraful reactiv: producători și consumatori, push-pull, de ce computed e lazy și glitch-free, problema diamantului și când rulează efectele.Reactivitate avansată
  • 14RxJS avansat: operatori de ordin superiorswitchMap, mergeMap, concatMap și exhaustMap pe o diagramă live, plus retry cu backoff, shareReplay și cum eviți scurgerile.Reactivitate avansată
  • 14NgRx SignalStoreState management la scară pe semnale: withState, withComputed, withMethods, rxMethod, entități și hooks — și când un store simplu e suficient.Reactivitate avansată
  • 14Mașini de stări cu signalsFără stări imposibile: o mașină tipizată pe signals, cu tranziții, guards și diagrama stărilor, live.Reactivitate avansată
  • 15WebSockets: timp real care rezistă rețeleiUn socket tipizat ca signals: reconectare cu backoff și jitter, heartbeat, coadă de mesaje cât ești offline și siguranță la SSR.Timp real și offline
  • 15Actualizări optimiste și rollbackInterfață instantanee, dar onestă: snapshot și rollback, răspunsuri sosite în altă ordine, chei de idempotență.Timp real și offline
  • 15Offline-first: Service Worker și cacheService Worker-ul Angular: strategii de cache, anunțarea versiunilor noi și o coadă pentru modificările făcute offline.Timp real și offline
  • 16Web WorkersCalcule grele fără să blochezi interfața: un worker real în pagină, comunicare prin mesaje și când nu merită.UI & performanță avansată
  • 16Liste uriașe: virtual scroll100.000 de rânduri fără lag: CDK virtual scroll, itemSize, trackBy și alternativele (paginare, infinite scroll).UI & performanță avansată
  • 16Animații moderne & View Transitionsanimate.enter și animate.leave în locul @angular/animations (depreciat), tranziții CSS și View Transitions la navigare.UI & performanță avansată
  • 16Memory leaks: cum le găsești și le repariSubscripții, timere și listeneri care trăiesc mai mult decât componenta lor — cum îi vezi în DevTools și cum îi închizi.UI & performanță avansată
  • 17Securitate: XSS, sanitizare & CSPCe face Angular automat împotriva XSS, când devine periculos bypassSecurityTrust, plus CSP cu nonce, Trusted Types și XSRF.Securitate & globalizare
  • 17Autentificare: token-uri & refreshUn interceptor care atașează token-ul, reîmprospătează o singură dată la 401 și reia cererile din coadă — plus unde ții token-ul.Securitate & globalizare
  • 17OAuth 2.1 și OpenID Connect cu PKCEFluxul authorization code cu PKCE, pas cu pas: verifier și challenge, state și nonce, ID token-ul și de ce un BFF e mai sigur.Securitate & globalizare
  • 17Internaționalizare (i18n)$localize, atributul i18n, ICU plural/select, build per limbă vs. traduceri la runtime și formatarea după locale.Securitate & globalizare