SSR #
Server-Side Rendering adalah pendekatan di mana server menghasilkan HTML lengkap — sudah dengan data — sebelum dikirim ke browser. User melihat konten nyata segera setelah response tiba, bukan blank page yang menunggu JavaScript selesai dieksekusi. Ini membuat SSR unggul untuk SEO, first load performance, dan halaman yang kontennya perlu diindeks mesin pencari. Tapi SSR punya biaya sendiri: setiap request membutuhkan server untuk render HTML, yang meningkatkan beban komputasi dan kompleksitas infrastruktur. Artikel ini membahas cara kerja SSR secara mendalam, konsep hydration, strategi caching yang kritis, streaming HTML, dan kapan SSR adalah pilihan yang tepat versus CSR atau Static Site Generation.
Cara Kerja SSR #
Di SSR, tanggung jawab rendering ada di server — bukan browser. Server menerima request, mengambil data yang diperlukan, me-render HTML lengkap, lalu mengirimkannya ke browser.
sequenceDiagram
participant B as Browser
participant S as SSR Server
participant DB as Database / API
B->>S: GET /products/laptop-gaming
S->>DB: Fetch product data, reviews, related products
DB-->>S: Data tersedia
Note over S: Render HTML lengkap di server<br/>(React.renderToString / template engine)
S-->>B: HTML lengkap dengan konten
Note over B: User langsung melihat konten!<br/>Tidak ada blank page.
B->>S: GET /app.js (hydration bundle)
S-->>B: JavaScript bundle
Note over B: Hydration: JavaScript "menghidupkan"<br/>HTML yang sudah ada di DOM
Note over B: Halaman fully interactive (event handlers attached)Dibandingkan CSR, SSR memberikan konten yang bisa dilihat jauh lebih cepat:
Timeline perbandingan SSR vs CSR untuk halaman produk:
SSR:
T=0ms: Browser kirim request
T=200ms: HTML lengkap dengan produk, harga, deskripsi diterima
→ User langsung melihat konten
T=500ms: JavaScript bundle dimuat
T=700ms: Hydration selesai → halaman fully interactive
CSR:
T=0ms: Browser kirim request
T=50ms: HTML kosong diterima → blank page
T=600ms: JavaScript bundle dimuat → loading spinner
T=900ms: API call untuk data produk selesai
T=1000ms: Halaman fully interactive
Perbedaan FCP (First Contentful Paint):
SSR: ~200ms
CSR: ~600ms
→ SSR 3x lebih cepat dalam menampilkan konten pertama
Hydration — Jembatan SSR dan Interaktivitas #
Hydration adalah proses di mana JavaScript mengambil alih HTML yang sudah di-render server dan menambahkan event handler, state, dan interaktivitas. Ini adalah konsep kunci yang membedakan SSR dari plain HTML.
Proses hydration:
1. Server render HTML dengan data:
<div id="product-card">
<h1>Laptop Gaming X1</h1>
<span class="price">Rp 15.000.000</span>
<button>Tambah ke Keranjang</button>
</div>
2. Browser terima dan tampilkan HTML → user sudah bisa baca konten
3. JavaScript bundle dimuat dan dieksekusi
4. React/Vue "hydrate" HTML yang sudah ada:
- Tidak membuat DOM baru
- Attach event listener ke elemen yang sudah ada
- Inisialisasi state dari data yang sudah ada di HTML
5. Halaman sekarang interactive (tombol bisa diklik, dll)
Potensi masalah hydration:
Hydration Mismatch:
Terjadi ketika HTML yang di-render server berbeda dari
yang akan di-render React di client
→ React membuang HTML server dan re-render dari nol
→ Ini menghilangkan manfaat SSR untuk interaktivitas awal
Penyebab umum:
- Tanggal/waktu yang di-render berbeda di server vs client
- Random values (Math.random()) yang berbeda setiap render
- Kondisi yang bergantung pada window/document (hanya ada di browser)
- localStorage/sessionStorage access saat render
// ANTI-PATTERN: Akses window saat render (menyebabkan hydration mismatch)
function ProductCard({ product }) {
const isDarkMode = window.matchMedia('(prefers-color-scheme: dark)').matches
// Error! window tidak ada di server
return <div className={isDarkMode ? 'dark' : 'light'}>...</div>
}
// BENAR: Gunakan useEffect untuk akses browser-only API
function ProductCard({ product }) {
const [isDarkMode, setIsDarkMode] = useState(false) // default untuk SSR
useEffect(() => {
// useEffect hanya berjalan di browser, tidak di server
setIsDarkMode(window.matchMedia('(prefers-color-scheme: dark)').matches)
}, [])
return <div className={isDarkMode ? 'dark' : 'light'}>...</div>
}
TTFB — Bottleneck Utama SSR #
Time to First Byte (TTFB) adalah waktu dari request dikirim sampai byte pertama response diterima. Di SSR, TTFB mencerminkan waktu yang dibutuhkan server untuk fetch data dan render HTML. Ini adalah metrik yang paling kritis dan paling sering menjadi bottleneck.
flowchart LR
Request["Browser\nRequest"]
subgraph Server["Server Processing (TTFB)"]
Auth["Autentikasi\n& Session\n~10ms"]
DB["Database\nQuery\n~50-200ms"]
API["External API\nCall\n~100-500ms"]
Render["HTML\nRendering\n~10-50ms"]
end
Response["HTML\nResponse"]
Request --> Auth --> DB --> API --> Render --> Response
style DB fill:#E67E22,color:#fff
style API fill:#E74C3C,color:#fffTarget TTFB yang baik: < 600ms (Google recommendation)
Target TTFB yang excellent: < 200ms
Yang paling sering memperlambat TTFB:
1. Database query yang lambat atau N+1 query
Solusi: optimize query, tambahkan index, gunakan connection pool
2. External API call yang lambat
Solusi: cache response API, set timeout yang ketat, fallback ke data stale
3. Tidak ada caching di level server
Solusi: cache hasil render atau cache data query
4. Autentikasi yang mahal (decode JWT per request)
Solusi: cache hasil validasi token
Cara mengukur TTFB:
- Chrome DevTools → Network tab → TTFB column
- Web Vitals extension
- server-timing header untuk breakdown per komponen
Strategi Caching untuk SSR #
Caching adalah yang paling penting untuk membuat SSR scalable. Tanpa caching, setiap request memaksa server untuk melakukan full render — yang mahal secara komputasi.
Level caching SSR dari yang paling dekat ke user:
1. CDN / Edge Cache (paling efektif)
→ Cache HTML yang sudah di-render di edge nodes (CloudFlare, Fastly)
→ Request tidak pernah mencapai server origin
→ Latency sangat rendah (server edge biasanya dekat secara geografis)
Cocok untuk: halaman yang sama untuk semua user (produk, artikel, landing page)
Tidak cocok untuk: halaman yang personal (dashboard user, cart, profil)
// Cache-Control header untuk CDN:
Cache-Control: public, s-maxage=300, stale-while-revalidate=600
// Cache di CDN 5 menit, serve stale sambil refresh di background 10 menit
2. Full Page Cache di Server
→ Simpan hasil render HTML di Redis atau memory
→ Subsequent requests untuk URL yang sama langsung return cached HTML
// Contoh implementasi cache di Next.js API / Express:
const cache = new Map()
async function getProduct(id) {
const cacheKey = `product:${id}`
if (cache.has(cacheKey)) {
return cache.get(cacheKey)
}
const product = await db.fetchProduct(id)
cache.set(cacheKey, product)
setTimeout(() => cache.delete(cacheKey), 5 * 60 * 1000) // TTL 5 menit
return product
}
3. Data Cache (tidak menyimpan HTML, tapi menyimpan data)
→ Cache hasil query database atau API response
→ Render tetap terjadi, tapi dengan data yang sudah tersedia di memory
Cocok untuk: data yang sama dipakai banyak halaman berbeda
Lebih fleksibel dari full page cache
4. Stale-While-Revalidate
→ Serve konten lama (stale) sambil refresh di background
→ User tidak pernah menunggu refresh
→ Konten mungkin sedikit outdated, tapi response selalu cepat
Ideal untuk: konten yang update sering tapi tolerance terhadap slight staleness
Contoh: harga saham yang update tiap menit (tolerable 1-2 menit delay)
Jangan cache halaman yang mengandung data personal user seperti dashboard, profil, atau cart. Cache CDN yang terlalu agresif bisa menyebabkan data user A terlihat oleh user B — security incident yang serius. Selalu include Vary: Cookie, Authorization header untuk halaman yang bergantung pada session, sehingga CDN memperlakukan setiap session sebagai cache key yang berbeda.SSR vs SSG — Kapan Memilih Mana #
Static Site Generation (SSG) adalah variasi SSR di mana HTML di-generate saat build time, bukan saat request time. Keduanya menghasilkan HTML yang bisa langsung ditampilkan browser, tapi dengan trade-off yang sangat berbeda.
flowchart TD
Q1{"Seberapa sering\ndata berubah?"}
Q2{"Apakah konten\nberbeda per user?"}
Q3{"Berapa banyak\nhalaman unik?"}
SSG["Static Site Generation\nBuild HTML saat deploy\nPaling cepat, paling murah\nContoh: blog, docs, landing page"]
ISR["Incremental Static\nRegeneration (ISR)\nRe-generate per interval\nNext.js: revalidate"]
SSR["Server-Side Rendering\nRender saat request\nFresh data, tapi ada server cost"]
CSR["Client-Side Rendering\nData fetch di browser\nFleksibel untuk personal content"]
Q1 -->|"Tidak pernah / jarang\n(blog posts, docs)"| SSG
Q1 -->|"Berkala\n(harga produk, berita)"| Q2
Q1 -->|"Real-time\n(live data, feed)"| Q2
Q2 -->|"Tidak (konten publik)"| Q3
Q2 -->|"Ya (per user)"| CSR
Q3 -->|"Banyak / tidak terbatas\n(e-commerce catalog)"| ISR
Q3 -->|"Terbatas\n(< 10.000 halaman)"| SSGPerbandingan ringkas:
Metode Build Time Request Time Cache SEO Personalisasi
SSG Lambat Sangat cepat Mudah ✓✓✓ Tidak
ISR Cepat Cepat Otomatis ✓✓✓ Tidak
SSR Cepat Sedang Manual ✓✓ Terbatas
CSR Cepat Lambat Client ✗ ✓✓✓
Panduan per use case:
Blog / Documentation → SSG
Landing page marketing → SSG
E-commerce product catalog → SSG + ISR
Halaman produk dengan stok → SSR atau ISR dengan revalidate pendek
News portal → SSR atau ISR
Dashboard personal → SSR (jika butuh SEO) atau CSR
Feed media sosial → CSR atau SSR dengan streaming
Streaming HTML — SSR yang Lebih Cepat #
Salah satu kelemahan SSR tradisional adalah server harus selesai render seluruh halaman sebelum bisa mengirimkan byte pertama. Streaming HTML mengatasi ini dengan mengirimkan HTML secara bertahap — bagian yang sudah selesai langsung dikirim, sementara bagian yang bergantung data lambat menyusul.
SSR Tradisional:
Server fetch semua data → render semua HTML → kirim sekaligus
Browser menunggu hingga seluruh halaman selesai
SSR dengan Streaming:
Server langsung kirim header HTML + bagian yang tidak perlu data
→ Browser mulai parse dan render shell halaman
Sambil itu, server fetch data untuk setiap section
→ Saat data tersedia, server kirim chunk HTML untuk section itu
→ Browser menampilkan section itu
Total waktu yang user rasakan: jauh lebih cepat
React 18 Suspense dengan Streaming:
// Di server (Next.js 13+ App Router):
export default async function ProductPage({ params }) {
// Langsung render halaman dengan Suspense boundary
return (
<main>
<ProductHeader /> {/* Tidak butuh data — langsung dikirim */}
<Suspense fallback={<ProductDetailSkeleton />}>
<ProductDetails id={params.id} /> {/* Fetch data, streaming saat ready */}
</Suspense>
<Suspense fallback={<ReviewsSkeleton />}>
<ProductReviews id={params.id} /> {/* Fetch data, streaming saat ready */}
</Suspense>
</main>
)
}
// ProductDetails melakukan data fetch secara langsung:
async function ProductDetails({ id }) {
const product = await fetchProduct(id) // server-side fetch
return <div>...</div>
}
Partial Hydration dan Islands Architecture #
Salah satu biaya tersembunyi SSR adalah hydration — browser harus mengeksekusi JavaScript untuk seluruh halaman, bahkan untuk bagian yang tidak interaktif. Partial Hydration dan Islands Architecture mengatasi ini.
Full Hydration (traditional SSR):
Seluruh halaman di-hydrate dengan JavaScript
→ Artikel blog yang 95% static tetap butuh hydrate semuanya
→ JavaScript bundle besar, Time to Interactive lambat
Partial Hydration / Islands Architecture:
Hanya "islands" (pulau) interaktif yang di-hydrate
→ Header navigasi (interactive) → hydrate
→ Body artikel (static) → tidak perlu hydrate
→ Comment section (interactive) → hydrate
→ Related articles (static links) → tidak perlu hydrate
Keuntungan:
→ JavaScript yang dikirim ke browser jauh lebih sedikit
→ Time to Interactive jauh lebih cepat
→ Cocok untuk content-heavy sites
Framework yang mendukung Islands Architecture:
→ Astro (islands per komponen)
→ Fresh (Deno-based)
→ Marko
→ Qwik (resumability, bukan hydration)
Optimasi TTFB di SSR #
// 1. Parallel data fetching — jangan sequential
// ANTI-PATTERN: Sequential fetch
async function getServerSideProps({ params }) {
const product = await fetchProduct(params.id) // 200ms
const reviews = await fetchReviews(params.id) // 150ms
const related = await fetchRelatedProducts(params.id) // 100ms
// Total: 450ms sequential
}
// BENAR: Parallel fetch
async function getServerSideProps({ params }) {
const [product, reviews, related] = await Promise.all([
fetchProduct(params.id),
fetchReviews(params.id),
fetchRelatedProducts(params.id),
])
// Total: ~200ms (waktu fetch terlama)
}
// 2. Timeout untuk external dependency
async function fetchWithTimeout(url, timeoutMs = 2000) {
const controller = new AbortController()
const timeoutId = setTimeout(() => controller.abort(), timeoutMs)
try {
const response = await fetch(url, { signal: controller.signal })
return await response.json()
} catch (error) {
if (error.name === 'AbortError') {
console.warn(`Fetch timeout: ${url}`)
return null // Fallback ke null, render halaman tanpa data ini
}
throw error
} finally {
clearTimeout(timeoutId)
}
}
// 3. Server-side cache untuk data yang sering diakses
const productCache = new Map()
async function getCachedProduct(id) {
if (productCache.has(id)) {
return productCache.get(id)
}
const product = await db.findProduct(id)
productCache.set(id, product)
// TTL cleanup
setTimeout(() => productCache.delete(id), 60 * 1000)
return product
}
Anti-Pattern SSR yang Harus Dihindari #
Blocking Render pada Data yang Tidak Kritis #
// ✗ Anti-pattern: Halaman tidak bisa di-render sampai semua data siap
async function getServerSideProps() {
const mainContent = await fetchMainContent() // kritis — 100ms
const recommendations = await fetchAI() // tidak kritis — 2000ms!
const ads = await fetchAds() // tidak kritis — 500ms
// User harus menunggu 2600ms karena recommendations lambat
return { props: { mainContent, recommendations, ads } }
}
// ✓ Solusi: Render konten kritis segera, load yang tidak kritis di client
async function getServerSideProps() {
const mainContent = await fetchMainContent() // hanya fetch yang kritis
return { props: { mainContent } }
// recommendations dan ads di-fetch di client menggunakan useEffect
}
Tidak Ada Timeout untuk External Dependencies #
// ✗ Anti-pattern: external call tanpa timeout
const reviews = await fetch('https://external-review-service.com/reviews')
// Jika service ini lambat atau down, halaman hang selamanya!
// ✓ Solusi: Selalu ada timeout dan graceful fallback
try {
const reviews = await fetchWithTimeout(
'https://external-review-service.com/reviews',
1500 // 1.5 detik timeout
)
return { props: { reviews } }
} catch {
// Fallback: render halaman tanpa reviews
return { props: { reviews: [] } }
}
Cache yang Terlalu Agresif untuk Halaman Personal #
// ✗ Anti-pattern: Cache halaman personal di CDN
Cache-Control: public, s-maxage=3600
// User A melihat dashboard user B!
// ✓ Solusi: Bedakan cache header berdasarkan tipe halaman
// Halaman publik (produk, artikel):
Cache-Control: public, s-maxage=300, stale-while-revalidate=600
// Halaman personal (dashboard, profil, cart):
Cache-Control: private, no-store
// Atau: Cache-Control: no-cache (revalidate setiap request)
Vary: Cookie, Authorization
Kapan Menggunakan SSR #
SSR tepat digunakan jika:
✓ Konten publik yang butuh SEO (blog, e-commerce, berita)
✓ First load performance sangat kritis (landing page, homepage)
✓ Konten yang di-share di media sosial (butuh meta tags di HTML awal)
✓ Pengguna dengan koneksi lambat atau perangkat low-end (tidak ada JS parsing berat)
✓ Konten yang personalized tapi masih butuh SEO (halaman profil publik)
SSR kurang tepat jika:
✗ Aplikasi yang sangat interaktif dan tidak butuh SEO (admin panel, dashboard)
✗ Data yang berubah sangat cepat (real-time yang butuh WebSocket)
✗ Tim tidak punya infrastructure untuk menjalankan server Node.js / SSR runtime
✗ Budget infrastruktur sangat terbatas (SSG jauh lebih murah untuk konten static)
Pertimbangkan SSG sebagai gantinya jika:
✓ Konten jarang berubah (dokumentasi, blog yang diupdate mingguan)
✓ Jumlah halaman terbatas dan bisa di-build dalam waktu yang wajar
✓ Tidak ada data per-user yang perlu di-render di server
Checklist SSR #
PERFORMA:
□ TTFB dipantau dan target < 600ms
□ Data fetch dilakukan secara parallel, bukan sequential
□ Timeout ada untuk semua external dependency
□ Server-side cache ada untuk data yang sering diakses
CACHING:
□ CDN cache dikonfigurasi untuk halaman publik
□ Cache-Control header berbeda untuk halaman public vs private
□ Vary header disertakan untuk halaman yang bergantung session
□ Stale-while-revalidate digunakan untuk konten yang bisa sedikit stale
HYDRATION:
□ Tidak ada akses ke window/document saat render (hanya di useEffect)
□ Server dan client render menghasilkan HTML yang identik
□ Suspense boundaries ada untuk konten yang datanya lambat
□ Hydration mismatch dimonitor di production
SEO:
□ Meta tags (title, description, og:image) ada di HTML yang dikirim server
□ Structured data (JSON-LD) tersedia di HTML awal
□ Canonical URL dikonfigurasi dengan benar
□ sitemap.xml dan robots.txt tersedia
RELIABILITY:
□ Error page (404, 500) di-render dengan baik di server
□ Fallback tersedia jika data fetch gagal
□ SSR tidak crash jika external service down
MONITORING:
□ Server render time dipantau (bukan hanya response time total)
□ Cache hit rate dipantau
□ Error rate di SSR dipantau terpisah dari client error
Ringkasan #
- SSR mengirim HTML lengkap dengan data — browser langsung menampilkan konten tanpa menunggu JavaScript, memberikan First Contentful Paint yang jauh lebih cepat dari CSR.
- Hydration menambahkan interaktivitas ke HTML yang sudah ada — bukan membuat DOM baru. Pastikan server dan client render menghasilkan output yang identik untuk menghindari hydration mismatch.
- TTFB adalah metrik yang paling kritis di SSR — setiap millisecond dalam server processing (query DB, external API) langsung dirasakan user. Parallel data fetching dan server-side cache adalah cara terbaik memperbaikinya.
- Caching adalah kunci scalability SSR — tanpa caching, setiap request memaksa full render. CDN cache untuk halaman publik bisa mengurangi beban server hampir 100%.
- Jangan cache halaman personal di CDN — ini adalah security issue serius. Gunakan
Cache-Control: private, no-storeuntuk halaman yang mengandung data user.- SSG lebih baik dari SSR untuk konten yang jarang berubah — HTML di-generate sekali saat build, bisa di-serve langsung dari CDN tanpa server involvement. Jauh lebih cepat dan lebih murah.
- ISR (Incremental Static Regeneration) mengisi gap SSR dan SSG — generate static HTML tapi dengan kemampuan re-generate secara berkala atau on-demand. Cocok untuk e-commerce catalog.
- Streaming SSR mengurangi perceived loading time — kirim shell halaman segera, stream section-by-section saat data tersedia. User melihat progress, bukan blank screen menunggu full render.
- Partial Hydration mengurangi JavaScript yang dikirim ke browser — untuk content-heavy sites, tidak semua bagian perlu dihydrate. Hanya komponen interaktif yang perlu JavaScript.
- Timeout untuk semua external dependency adalah wajib — SSR tanpa timeout pada external API call bisa menyebabkan seluruh halaman hang jika service eksternal lambat atau down.