Kuasai Next.js rendering strategy. Pelajari bagaimana CSR, SSR, ISR, dan SSG bekerja under the hood, kapan menggunakan setiap approach, dan cara mengimplementasikannya untuk optimal performance dan SEO.

Next.js adalah React framework yang memungkinkan Anda memilih bagaimana dan kapan untuk render page Anda. Fleksibilitas ini powerful tetapi confusing. Haruskah Anda render di client? Di server? Pada build time? On-demand?
Jawabannya tergantung pada use case Anda. Marketing homepage memiliki requirement berbeda daripada real-time dashboard. Product catalog berbeda dari user profile page.
Panduan ini menjelaskan bagaimana setiap rendering strategy bekerja, mengapa penting, dan kapan menggunakannya. Kami akan focus pada App Router, modern Next.js approach.
Ketika Anda mengunjungi website, browser Anda membuat HTTP request. Server merespons dengan HTML. Browser Anda parse HTML, download CSS dan JavaScript, execute JavaScript, dan render page.
Pertanyaannya adalah: dari mana HTML berasal?
Client-side rendering (CSR): Server mengirim empty HTML shell. JavaScript berjalan dalam browser, fetch data, dan render page.
Server-side rendering (SSR): Server generate complete HTML dengan data sudah included. Browser menerima fully-rendered page.
Static site generation (SSG): HTML di-generate sekali pada build time. HTML yang sama disajikan ke setiap user.
Incremental static regeneration (ISR): HTML di-generate pada build time, tetapi dapat di-regenerate on-demand atau pada schedule.
Setiap approach memiliki trade-off. CSR cepat untuk deploy tetapi lambat untuk user. SSR lebih lambat untuk deploy tetapi cepat untuk user. SSG paling cepat tetapi memerlukan rebuild untuk content change. ISR balance keduanya.
Dalam Next.js, hydration adalah proses di mana JavaScript take over server-rendered page. Server mengirim HTML. Browser render. Kemudian JavaScript berjalan dan membuat page interactive.
Ini penting. Server-rendered page tanpa JavaScript adalah static. Hydration membuatnya interactive.
'use client'
import { useState } from 'react'
export default function Counter() {
const [count, setCount] = useState(0)
return (
<div>
<p>Count: {count}</p>
<button onClick={() => setCount(count + 1)}>
Increment
</button>
</div>
)
}Ketika component ini di-server-render, server mengirim HTML dengan Count: 0. Button ada tetapi tidak bekerja. Ketika JavaScript load, React hydrate component, attach event listener, dan button menjadi interactive.
Client-side rendering berarti browser melakukan semua pekerjaan. Server mengirim minimal HTML. JavaScript berjalan dalam browser, fetch data, dan render page.
'use client'
import { useEffect, useState } from 'react'
export default function Products() {
const [products, setProducts] = useState([])
const [loading, setLoading] = useState(true)
useEffect(() => {
fetch('/api/products')
.then(res => res.json())
.then(data => {
setProducts(data)
setLoading(false)
})
}, [])
if (loading) return <div>Loading...</div>
return (
<div>
{products.map(product => (
<div key={product.id}>{product.name}</div>
))}
</div>
)
}Berikut adalah apa yang terjadi:
/api/productsCSR ideal untuk:
Server-side rendering berarti server generate complete HTML untuk setiap request. Browser menerima fully-rendered page.
import { Suspense } from 'react'
async function getProducts() {
const res = await fetch('https://api.example.com/products', {
cache: 'no-store'
})
return res.json()
}
async function ProductList() {
const products = await getProducts()
return (
<div>
{products.map(product => (
<div key={product.id}>{product.name}</div>
))}
</div>
)
}
export default function Products() {
return (
<Suspense fallback={<div>Loading...</div>}>
<ProductList />
</Suspense>
)
}Berikut adalah apa yang terjadi:
SSR ideal untuk:
Static site generation berarti HTML di-generate sekali pada build time. HTML yang sama disajikan ke setiap user.
async function getProducts() {
const res = await fetch('https://api.example.com/products')
return res.json()
}
export async function generateStaticParams() {
const products = await getProducts()
return products.map(product => ({
id: product.id.toString()
}))
}
export default function Product({ params }) {
// Ini berjalan pada build time, bukan request time
return <div>Product {params.id}</div>
}Berikut adalah apa yang terjadi:
generateStaticParams()SSG ideal untuk:
ISR menggabungkan best dari SSG dan SSR. HTML di-generate pada build time, tetapi dapat di-regenerate on-demand atau pada schedule.
async function getProduct(id) {
const res = await fetch(`https://api.example.com/products/${id}`)
return res.json()
}
export async function generateStaticParams() {
const products = await fetch('https://api.example.com/products').then(r => r.json())
return products.slice(0, 100).map(p => ({ id: p.id.toString() }))
}
export const revalidate = 60 // Regenerate setiap 60 detik
export default function Product({ params }) {
const product = getProduct(params.id)
return <div>{product.name}</div>
}Berikut adalah apa yang terjadi:
Anda juga dapat regenerate HTML on-demand menggunakan revalidateTag() function:
async function getProduct(id) {
const res = await fetch(`https://api.example.com/products/${id}`, {
next: { tags: ['products'] }
})
return res.json()
}
export const revalidate = false // Jangan pernah auto-regenerate
export default function Product({ params }) {
const product = getProduct(params.id)
return <div>{product.name}</div>
}Kemudian, ketika data change, call revalidation API:
import { revalidateTag } from 'next/cache'
export async function POST(request) {
const secret = request.headers.get('x-api-secret')
if (secret !== process.env.REVALIDATION_SECRET) {
return new Response('Unauthorized', { status: 401 })
}
revalidateTag('products')
return new Response('Revalidated', { status: 200 })
}ISR ideal untuk:
Streaming adalah technique di mana server mengirim HTML dalam chunk daripada semuanya sekaligus. Browser mulai render sebelum entire page siap.
import { Suspense } from 'react'
async function SlowComponent() {
await new Promise(resolve => setTimeout(resolve, 3000))
return <div>Ini memakan 3 detik untuk load</div>
}
export default function Page() {
return (
<div>
<h1>Fast content</h1>
<Suspense fallback={<div>Loading...</div>}>
<SlowComponent />
</Suspense>
</div>
)
}Berikut adalah apa yang terjadi:
User melihat content lebih cepat, bahkan meskipun total time sama.
Streaming ideal untuk:
Apakah content sama untuk semua user?
Apakah content change frequently?
Apakah SEO penting?
Apakah Anda perlu real-time data?
Apakah page behind authentication?
Blog homepage: SSG. Content adalah static. Regenerate daily atau on-demand ketika new post dipublikasikan.
Blog post: SSG. Content adalah static. Regenerate ketika post di-update.
Product page: ISR. Content mostly static tetapi price dan inventory change. Regenerate setiap jam atau on-demand.
Shopping cart: CSR. Highly interactive. User-specific. Tidak ada server-side rendering yang diperlukan.
User dashboard: CSR atau SSR. User-specific data. SSR jika SEO penting, CSR jika tidak.
Search result: SSR. Dynamic content berdasarkan query. Tidak dapat pre-render semua possibility.
Real-time stock ticker: CSR. Data change constantly. Server-side rendering adalah wasteful.
'use client'
import { useEffect, useState } from 'react'
export default function Page() {
const [data, setData] = useState(null)
const [loading, setLoading] = useState(true)
const [error, setError] = useState(null)
useEffect(() => {
fetch('/api/data')
.then(res => res.json())
.then(data => {
setData(data)
setLoading(false)
})
.catch(err => {
setError(err)
setLoading(false)
})
}, [])
if (loading) return <div>Loading...</div>
if (error) return <div>Error: {error.message}</div>
return <div>{data.content}</div>
}async function getData() {
const res = await fetch('https://api.example.com/data', {
cache: 'no-store'
})
return res.json()
}
export default async function Page() {
const data = await getData()
return <div>{data.content}</div>
}async function getData() {
const res = await fetch('https://api.example.com/data')
return res.json()
}
export const revalidate = false // Jangan pernah revalidate
export default async function Page() {
const data = await getData()
return <div>{data.content}</div>
}async function getData() {
const res = await fetch('https://api.example.com/data')
return res.json()
}
export const revalidate = 3600 // Revalidate setiap jam
export default async function Page() {
const data = await getData()
return <div>{data.content}</div>
}async function getData() {
const res = await fetch('https://api.example.com/data', {
cache: 'no-store' // Selalu fetch fresh
})
return res.json()
}
export default async function Page() {
const data = await getData()
return <div>{data.content}</div>
}Jika data tidak change, gunakan SSG:
async function getData() {
const res = await fetch('https://api.example.com/data')
return res.json()
}
export const revalidate = false
export default async function Page() {
const data = await getData()
return <div>{data.content}</div>
}'use client'
import { useEffect, useState } from 'react'
export default function Page() {
const [data, setData] = useState(null)
useEffect(() => {
fetch('/api/data').then(res => res.json()).then(setData)
}, [])
return <div>{data.content}</div> // Crash jika data adalah null
}Selalu handle loading dan error state:
'use client'
import { useEffect, useState } from 'react'
export default function Page() {
const [data, setData] = useState(null)
const [loading, setLoading] = useState(true)
useEffect(() => {
fetch('/api/data')
.then(res => res.json())
.then(data => {
setData(data)
setLoading(false)
})
}, [])
if (loading) return <div>Loading...</div>
if (!data) return <div>No data</div>
return <div>{data.content}</div>
}async function getData() {
const res = await fetch('https://api.example.com/data', {
cache: 'no-store' // Fetch fresh setiap kali
})
return res.json()
}
export const revalidate = 3600 // Revalidate setiap jam
export default async function Page() {
const data = await getData()
return <div>{data.content}</div>
}cache: 'no-store' override revalidate. Gunakan cache: 'force-cache' untuk ISR:
async function getData() {
const res = await fetch('https://api.example.com/data', {
cache: 'force-cache' // Cache response
})
return res.json()
}
export const revalidate = 3600
export default async function Page() {
const data = await getData()
return <div>{data.content}</div>
}'use client'
import { useEffect } from 'react'
async function getData() {
'use server'
const res = await fetch('https://api.example.com/data')
return res.json()
}
export default function Page() {
useEffect(() => {
getData() // Ini tidak bekerja seperti yang diharapkan
}, [])
return <div>Page</div>
}Gunakan server component untuk data fetching:
async function getData() {
const res = await fetch('https://api.example.com/data')
return res.json()
}
export default async function Page() {
const data = await getData()
return <div>{data.content}</div>
}Untuk blog, documentation, dan product catalog, ISR adalah sweet spot. Generate page pada build time, regenerate on-demand atau pada schedule.
export const revalidate = 86400 // Regenerate daily
export async function generateStaticParams() {
const posts = await getPosts()
return posts.map(post => ({ slug: post.slug }))
}
export default async function Post({ params }) {
const post = await getPost(params.slug)
return <article>{post.content}</article>
}Page dapat menggunakan multiple strategy. Homepage mungkin SSG, tetapi sidebar mungkin CSR.
import { Suspense } from 'react'
// Server component - SSG
async function Header() {
const data = await fetch('https://api.example.com/header')
return <header>{data.title}</header>
}
// Client component - CSR
'use client'
function Sidebar() {
const [items, setItems] = useState([])
useEffect(() => {
fetch('/api/sidebar').then(r => r.json()).then(setItems)
}, [])
return <aside>{items.map(item => <div>{item}</div>)}</aside>
}
export default function Page() {
return (
<div>
<Header />
<Suspense fallback={<div>Loading sidebar...</div>}>
<Sidebar />
</Suspense>
</div>
)
}Rendering strategy berbeda mempengaruhi performance berbeda. Monitor:
SSG dan ISR typically memiliki best LCP. CSR mungkin memiliki worse LCP tetapi better FID jika page highly interactive.
Ketika content change, trigger revalidation immediately daripada menunggu schedule:
import { revalidatePath } from 'next/cache'
export async function POST(request) {
const secret = request.headers.get('x-webhook-secret')
if (secret !== process.env.WEBHOOK_SECRET) {
return new Response('Unauthorized', { status: 401 })
}
const { postId } = await request.json()
revalidatePath(`/blog/${postId}`)
return new Response('Revalidated', { status: 200 })
}next/image dengan priority untuk above-the-fold image.next/image dengan priority untuk critical image.next/image dengan lazy loading untuk non-critical image.import Image from 'next/image'
export default function Page() {
return (
<div>
<Image
src="/hero.jpg"
alt="Hero"
width={1200}
height={600}
priority // Load immediately
/>
<Image
src="/thumbnail.jpg"
alt="Thumbnail"
width={300}
height={300}
// Lazy load by default
/>
</div>
)
}Next.js rendering strategy adalah tool. Masing-masing memiliki strength dan weakness. Pilihan terbaik tergantung pada content, traffic, dan requirement Anda.
SSG adalah fastest dan cheapest. Gunakannya untuk static content.
ISR balance freshness dan performance. Gunakannya untuk mostly-static content yang change occasionally.
SSR menyediakan real-time data dan personalization. Gunakannya untuk dynamic content yang perlu SEO.
CSR adalah simple dan interactive. Gunakannya untuk highly interactive page dan personalized content.
Mulai dengan SSG atau ISR. Pindah ke SSR atau CSR hanya ketika necessary. Monitor performance dan adjust sesuai kebutuhan. User Anda akan berterima kasih.