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:
KeranjangStoremenulisprivate isi = signal<Item[]>(JSON.parse(localStorage.getItem('keranjang') ?? '[]')). Di browser ini jalan. Apa yang terjadi saatng buildme-render/menudi 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 |
- Dengan SSR, menu terbaca 1,4 detik lebih cepat, karena teksnya ada di HTML. Mesin pencari dan preview link di WhatsApp juga membaca teks itu.
- Tombolnya baru hidup di waktu yang hampir sama: dua-duanya menunggu
main.js(235,67 kB) diunduh dan dijalankan. nghadalah catatan untuk hydration,ng-server-context="ssg"artinya halaman ini di-render saat build (prerender), danjsactiondipakai untuk merekam klik sebelum Angular siap.
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 }
];
provideClientHydration()menyalakan hydration. Tempatnya diapp.config.ts, supaya browser dan server sama-sama memakainya (mergeApplicationConfigmenggabungkan keduanya).RenderMode.Prerenderme-render halaman saatng build;RenderMode.Serverme-render tiap ada request;RenderMode.Clientkembali ke CSR.
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
- API browser saat class dibuat.
localStorage,window, dandocumentdi field atau constructor membuat render server gagal. Pindahkan keafterNextRender. - Template yang beda di server dan browser. Cabang
@if (isBrowser)membuang kerja server; render yang sama, lalu perbarui setelah render. - HTML yang gak valid.
<table>tanpa<tbody>berakhir dengan NG0501. (<div>di dalam<p>sudah ditolak saat build:NG5002: Unexpected closing tag "p".) - Lupa
withEventReplay(). Klik di antara HTML tampil dan hydration selesai hilang. - Detail latihan. Starter-nya punya
export const serverConfig: ApplicationConfig = { providers: [] }. Tambahkanimport { provideClientHydration } from '@angular/platform-browser'dan isiproviders: [provideClientHydration()]. Penilai membaca kodenya: import-nya, dan pemanggilanprovideClientHydration()di dalamproviders. Di proyek sungguhan, baris itu ada diapp.config.ts, seperti di atas.
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.