React Server Components (RSC) adalah pergeseran paling signifikan dalam model pemrograman React sejak hooks. Di Next.js 15 dengan App Router, setiap komponen adalah Server Component secara default. Nol JavaScript untuk komponen render murni, tidak ada hidrasi, dan akses database langsung tanpa lapisan API. Tapi mereka bukan pengganti Client Components — mereka adalah pelengkap. Setelah membangun beberapa aplikasi App Router dari awal, inilah pemahaman praktis saya tentang cara menggunakannya secara efektif.
Perubahan fundamental: Server Components hanya berjalan di server dan mengirim HTML (bukan JavaScript) ke klien. Server Component yang mengambil dari Prisma tidak pernah mengirimkan klien Prisma, logika query database, atau kode terkait apa pun ke browser. Klien menerima HTML yang dirender. Ini menghilangkan seluruh kategori data fetching — tidak ada useEffect, tidak ada loading state untuk data awal, tidak ada fetch sisi klien.
Direktif 'use client' tidak membuat komponen tanpa server — ia menandai batas di mana pohon komponen menjadi dirender di klien. Semua yang di atas batas bisa menjadi Server Component. Kesalahan paling umum: menandai komponen induk 'use client' ketika hanya satu komponen daun yang membutuhkan interaktivitas. Anda bisa meneruskan output Server Component sebagai props (children) ke dalam Client Component.
Server Components (default) Client Components ('use client')
─────────────────────────── ─────────────────────────────────
✅ Direct database access ✅ useState, useEffect
✅ Async/await at component level ✅ Event handlers (onClick, onChange)
✅ 0 JS shipped to browser ✅ Browser APIs (window, localStorage)
✅ Server secrets (env vars) ✅ Real-time subscriptions
✅ Reduced client bundle ✅ Drag-and-drop
✅ Charts (DOM-dependent)
Server
┌─────────────────────────────────────────┐
│ page.tsx (Server Component) │
│ await prisma.user.findMany() ─── DB │
│ │ │
│ ▼ │
│ <UserList users={users}> │
│ └─ Server Component (renders HTML) │
│ │ │
│ <DataTable data={users}> ◄── 'use client'
│ └─ Client Component: sorting/filtering│
└─────────────────────────────────────────┘
HTML streamed to browser
─────────────────────────────────────────
Prisma client: never shipped to browser ✅
DataTable JS: shipped for interactivity ✅Dari membangun dashboard ERP dengan Next.js App Router: ambil data pada level Server Component tertinggi yang memungkinkan, lalu teruskan sebagai props ke Client Components interaktif di bawahnya. Server Component menjalankan query Prisma, meneruskan hasilnya sebagai prop ke komponen DataTable 'use client'. DataTable bisa melakukan pengurutan, penyaringan, dan paginasi sisi klien tanpa pernah menekan API lagi — semua data sudah ada dari render server awal.
Async/await di Server Components adalah default yang bersih. Server Component bisa async — await query Prisma, await fetch() ke API eksternal, await Redis.get() — dan merender hasilnya langsung. Beberapa data fetch independen dalam sebuah komponen harus menggunakan Promise.all() untuk paralelisasi daripada menunggu secara berurutan. Data yang bergantung (fetch B hanya setelah A mengembalikan) bisa menggunakan await berurutan, tapi pertimbangkan apakah streaming Suspense akan lebih melayani pengguna.
// ✅ Server Component with parallel data fetching
export default async function Dashboard() {
// These run in parallel (not waterfall)
const [users, orders, revenue] = await Promise.all([
prisma.user.count(),
prisma.order.findMany({ where: { status: 'PENDING' } }),
prisma.order.aggregate({ _sum: { total: true } }),
])
return (
<div>
<StatsCard users={users} revenue={revenue._sum.total} />
{/* Server Component passes data to Client Component */}
<OrdersTable orders={orders} /> {/* 'use client' inside */}
</div>
)
}
// ✅ Streaming with Suspense
export default function Page() {
return (
<div>
<QuickStats /> {/* Fast — renders immediately */}
<Suspense fallback={<TableSkeleton />}>
<SlowDataTable /> {/* Slow — streams when ready */}
</Suspense>
</div>
)
}
// ✅ Caching Prisma queries with unstable_cache
import { unstable_cache } from "next/cache"
const getProducts = unstable_cache(
async () => prisma.product.findMany(),
["products"], // cache key
{ revalidate: 3600 } // revalidate every hour
)
// ❌ Anti-pattern: fetching in Client Component unnecessarily
"use client"
export function UserProfile({ userId }) {
const [user, setUser] = useState(null)
useEffect(() => {
fetch(`/api/users/${userId}`).then(r => r.json()).then(setUser)
}, [userId])
// ↑ Doubles latency: server renders, HTML arrives, hydrates, then fetches again
// Fix: pass user as prop from Server Component parent
}Batas Suspense memungkinkan Anda secara progresif melakukan streaming output Server Component. Bungkus Server Component yang lambat dimuat dalam <Suspense fallback={<Skeleton />}> dan Next.js melakukan streaming halaman: bagian yang cepat dirender segera, bagian yang lambat dirender saat siap, menggantikan skeleton. Ini meningkatkan performa yang dirasakan secara signifikan.
Pola anti yang umum: Server Component merender Client Component, yang kemudian menggunakan useEffect untuk mengambil data yang sama yang bisa disediakan Server Component. Ini menggandakan latensi — server merender, HTML tiba, JS terhidrasi, lalu klien menembakkan fetch lain. Selalu ambil di Server Component dan teruskan sebagai props. Client Component harus menangani interaktivitas saja — mutasi, event pengguna, pembaruan real-time via WebSocket.
fetch() Next.js di Server Components memiliki caching otomatis. Secara default, respons di-cache per request (deduplikasi dalam satu request). Anda mengontrol caching yang lebih lama dengan { next: { revalidate: N } } (revalidasi berbasis waktu gaya ISR) atau { cache: 'no-store' } untuk data yang selalu segar. Query Prisma tidak melalui fetch(), jadi mereka tidak di-cache otomatis — gunakan wrapper unstable_cache() Next.js untuk menerapkan caching ke query Prisma.
Aturan saya: semua yang tidak membutuhkan useState, useEffect, event handler, atau API browser adalah Server Component. Daftar hal-hal yang membutuhkan Client Components lebih pendek dari yang Anda bayangkan: kontrol form dengan state terkontrol, modal dan drawer dengan state buka/tutup, chart (mereka mengakses DOM), interaksi drag-and-drop, dan langganan real-time. Dashboard ERP berat-RSC saya memuat jauh lebih cepat dari versi Pages Router sebelumnya.
Server Components sangat baik untuk SEO karena konten ada dalam respons HTML awal — tidak ada JavaScript yang diperlukan untuk mesin pencari melihatnya. Halaman produk, posting blog, dan halaman marketing mendapat manfaat paling banyak. API metadata Next.js (generateMetadata, ekspor objek metadata) juga bekerja di level Server Component — gambar OG dinamis, URL kanonik, dan data terstruktur semuanya dirender di sisi server.