SSR Angular — Angular

Server-Side Rendering (SSR) merender Angular di server untuk performa dan SEO lebih baik. Setup dengan Angular CLI ng add @angular/ssr Hydration Angular 17+…

Server-side rendering (SSR) artinya HTML halaman, lengkap dengan isinya, sudah dibuat di server sebelum dikirim. Browser bisa langsung menampilkan menu Bang Jali tanpa menunggu JavaScript-nya. Setelah JavaScript sampai, Angular mengambil alih HTML itu, gak menggambar ulang; langkah ini disebut hydration. Bayangin papan menu yang sudah ditulis dan dipasang di depan gerobak sebelum warung buka. Pembeli bisa membacanya dari jauh, tapi baru bisa memesan setelah kasirnya datang. Kasirnya gak menulis ulang papan itu; dia tinggal berdiri di belakangnya dan mulai melayani. Kalau ada yang sudah memencet bel sebelum kasir datang, kasir yang baik melayaninya begitu sampai.

🧪 Tebakan cepat: KeranjangStore menulis private isi = signal<Item[]>(JSON.parse(localStorage.getItem('keranjang') ?? '[]')). Di browser ini jalan. Apa yang terjadi saat ng build me-render /menu di server? Jawabannya ada di bagian "window dan localStorage gak ada di server".

Yang dikirim server: CSR vs SSR

Tanpa SSR (client-side rendering, CSR), server mengirim kerangka kosong. Ini isi <body>-nya untuk /menu:

<body>
  <app-root></app-root>
  <script src="polyfills-5CFQRCPP.js" type="module"></script>
  <script src="main-TWSZUFX7.js" type="module"></script>
</body>

Dengan SSR, menunya sudah ada di HTML (dipotong, apa adanya dari hasil build):

<app-root ng-version="20.3.33" ngh="1" ng-server-context="ssg">
  <h1>Martabak Bang Jali</h1>
  <app-menu ngh="0">
    <p> Martabak Manis Cokelat Keju: Rp40000 <button jsaction="click:;">Pesan</button></p>
    <p> Martabak Telur Spesial: Rp50000 <button jsaction="click:;">Pesan</button></p>
    <p class="badge">Keranjang: 0 item</p>
  </app-menu>
</app-root>

Aplikasi contoh dibuka di Chrome yang dilambatkan seperti HP (latency 150 ms, 1,6 Mbps, CPU 4x lebih lambat):

HTML sampai Menu tampil Tombol Pesan hidup
CSR 171 ms (kosong) 1.760 ms 1.714 ms
SSR 176 ms 384 ms 1.712 ms

Menyalakan SSR

Proyek baru: ng new bang-jali --ssr. Proyek yang sudah ada: ng add @angular/ssr. Dulu fitur ini bernama Angular Universal; sejak Angular 17 paketnya @angular/ssr. Perintah itu menambah src/server.ts (server Express), src/main.server.ts, app.config.server.ts, dan app.routes.server.ts. Ini isi yang dibuat ng new --ssr di Angular 20.3:

// src/app/app.config.ts: dipakai browser, dan ikut digabung ke config server
export const appConfig: ApplicationConfig = {
  providers: [
    provideBrowserGlobalErrorListeners(),
    provideZoneChangeDetection({ eventCoalescing: true }),
    provideRouter(routes), provideClientHydration(withEventReplay())
  ]
};

// src/app/app.config.server.ts
const serverConfig: ApplicationConfig = {
  providers: [
    provideServerRendering(withRoutes(serverRoutes))
  ]
};

export const config = mergeApplicationConfig(appConfig, serverConfig);

// src/app/app.routes.server.ts
export const serverRoutes: ServerRoute[] = [
  { path: '**', renderMode: RenderMode.Prerender }
];

ng build sekarang membuat dua bundle dan mencetak Prerendered 2 static routes. di akhir. Hasilnya ada di dist/bang-jali/browser (file untuk browser, termasuk menu/index.html) dan dist/bang-jali/server, dan npm run serve:ssr:bang-jali menjalankan node dist/bang-jali/server/server.mjs di port 4000.

Hydration

Di browser, Angular mencocokkan DOM dari server dengan template-nya dan memakai node yang sudah ada. Di mode development, console menulis Angular hydrated 2 component(s) and 22 node(s), 0 component(s) were skipped.

withEventReplay() merekam klik yang terjadi sebelum hydration selesai, lalu memutarnya ulang. Diukur: klik Pesan pada 430 ms, hydration selesai pada 1.710 ms, dan keranjangnya tetap jadi Keranjang: 1 item. Tanpa withEventReplay(), klik yang sama hilang dan keranjangnya tetap 0.

Hydration butuh DOM browser sama persis dengan HTML dari server. Menu yang ditulis sebagai <table> berisi <tr> tanpa <tbody> merusaknya: browser menyisipkan <tbody> sendiri saat membaca HTML, lalu console menulis ERROR RuntimeError: NG0501: During hydration Angular expected more sibling nodes to be present., dan tombol Pesan gak lagi menambah keranjang. Menulis <tbody> sendiri di template memperbaikinya.

window dan localStorage gak ada di server

Saat SSR, komponen dan service juga jalan di Node.js, dan di sana gak ada window, document, atau localStorage. Versi tebakan tadi membuat ng build mencetak:

ERROR ReferenceError: localStorage is not defined

/menu gak punya file HTML hasil prerender, dan server Node menjawab request /menu dengan 404 Not Found. Itu jawaban tebakannya. Perbaikannya, baca localStorage di afterNextRender, yang cuma jalan di browser, setelah render pertama:

@Injectable({ providedIn: 'root' })
export class KeranjangStore {
  private isi = signal<Item[]>([])

  constructor() {
    afterNextRender(() => {
      const simpanan = localStorage.getItem('keranjang')
      if (simpanan) {
        this.isi.set(JSON.parse(simpanan))
      }
    })
  }
}

Hasilnya: HTML dari server menulis Keranjang: 0 item, lalu setelah hydration browser membaca dua item yang tersimpan dan badge-nya jadi Keranjang: 2 item. Untuk logika lain, isPlatformBrowser(inject(PLATFORM_ID)) bernilai true di browser dan false di server. Jangan pakai itu untuk merender template yang berbeda: Angular memang gak error, tapi HTML server untuk bagian itu dibuang dan digambar ulang.

📌 Menjalankan server hasil build

Di Angular 20.3, server hasil build menolak host yang gak terdaftar. Dijalankan di localhost:8818, server-nya mencetak Header "host" with value "localhost:8818" is not allowed. lalu Falling back to client side rendering., dan halamannya diam-diam kembali jadi CSR. Daftarkan host-nya, misalnya NG_ALLOWED_HOSTS=localhost node dist/bang-jali/server/server.mjs, atau isi security.allowedHosts di angular.json.

⚠️ Jebakan yang sering ketemu

TL;DR: SSR membuat HTML berisi konten di server (ng new --ssr atau ng add @angular/ssr, dulu bernama Angular Universal), jadi menu terbaca jauh lebih cepat (384 ms vs 1.760 ms di contoh) dan bisa dibaca mesin pencari. Hydration, dinyalakan dengan provideClientHydration(), membuat Angular di browser memakai DOM dari server alih-alih menggambar ulang; withEventReplay() memutar ulang klik yang datang lebih awal. DOM browser harus sama dengan HTML server (tulis <tbody> sendiri). Di server gak ada window, document, atau localStorage: baca di afterNextRender, bukan saat class dibuat, atau prerender gagal dengan ReferenceError: localStorage is not defined.

Yang akan kamu pelajari