📚 React Server Components — model render yang berubah
Status versi (terverifikasi): - React 19.3.0 rilis 9 Sep 2026; patch terakhir 19.3.1 (14 Okt 2026) — react.dev/versions - Next.js 16.4.0 rilis 6 Okt 2026; patch 16.4.3 (16 Okt 2026) — nextjs.org/blog/next-16-4 - RSC di React 19 stabil; API internal bundler-nya tidak ikut semver (dokumentasi resmi React) → pin versi React kalau bikin tooling/framework sendiri.
🎯 Tujuan Belajar
Bisa, besok:
1. Memutuskan komponen mana server, mana client — bukan berdasarkan "kebiasaan", tapi berdasarkan di mana data dan interaktivitas dibutuhkan.
2. Membaca dan memperbaiki error serialisasi RSC boundary.
3. Memakai 'use cache' + cacheLife untuk caching eksplisit, dan tahu kapan itu merugikan.
4. Menghindari pola yang bikin bundle client membengkak diam-diam.
🏗️ Arsitektur & Alur Teknis
DIKETAHUI (dari dokumen resmi React & Next.js):
RSC bukan SSR. Dua fase berbeda:
| Fase | Jalan di mana | Output |
|---|---|---|
| Server Component render | Node/edge runtime, sebelum bundling | Payload RSC (bukan HTML) |
| SSR | Server | HTML dari payload tadi |
| Hydration | Browser | Client Component jadi interaktif |
Konsekuensi penting:
- Kode Server Component tidak pernah masuk bundle client. Library berat (marked, sanitize-html, dsb.) tidak dikirim.
- Server Component tidak punya state, effect, event handler, browser API.
- Server Component boleh async dan await langsung di render.
- Client Component ('use client') tetap di-SSR lalu di-hydrate.
Alur request dengan Cache Components (Next 16.4):
request → static shell (prerendered, dari CDN/cache)
→ stream Suspense boundary (bagian dinamis)
→ client navigation: React <Activity> simpan state route lama
Poin yang sering salah dipahami: 'use client' bukan "render di client saja". Itu menandai boundary — modul itu dan semua import-nya ikut ke bundle client, dan tetap di-SSR sekali.
💻 Kode Nyata / Konfigurasi
1. Boundary dasar: server ambil data, client urus interaksi
// app/posts/[id]/page.tsx — Server Component (default, tanpa directive)
import { db } from '@/lib/db';
import { LikeButton } from './like-button';
export default async function PostPage({ params }: { params: Promise<{ id: string }> }) {
const { id } = await params; // Next 15+: params adalah Promise
const post = await db.post.findUnique({ where: { id } });
if (!post) return <p>Not found</p>;
return (
<article>
<h1>{post.title}</h1>
<p>{post.body}</p>
{/* Hanya angka yang menyeberang boundary — bukan seluruh objek post */}
<LikeButton postId={post.id} initialLikes={post.likes} />
</article>
);
}
// app/posts/[id]/like-button.tsx
'use client';
import { useState, useTransition } from 'react';
import { likePost } from './actions';
export function LikeButton({ postId, initialLikes }: { postId: string; initialLikes: number }) {
const [likes, setLikes] = useState(initialLikes);
const [pending, startTransition] = useTransition();
return (
<button
disabled={pending}
onClick={() => startTransition(async () => {
setLikes((n) => n + 1); // optimistic
await likePost(postId); // Server Action
})}
>
♥ {likes}
</button>
);
}
2. Serialisasi boundary — aturan yang bikin error
Yang boleh lewat server → client: string, number, boolean, null, plain object/array, Date, Map, Set, BigInt, Promise, JSX element, Server Action reference.
Yang tidak boleh: fungsi biasa, class instance, Symbol, dan apa pun yang tidak serializable.
// ❌ Error: Functions cannot be passed directly to Client Components
<Chart format={(n) => n.toFixed(2)} />
// ✅ Opsi A — jadikan Server Action
// actions.ts
'use server';
export async function formatCurrency(n: number) { return new Intl.NumberFormat('id-ID').format(n); }
<Chart formatAction={formatCurrency} />
// ✅ Opsi B — hitung di server, kirim hasilnya
<Chart labels={rows.map(r => formatCurrency(r.total))} values={rows.map(r => r.total)} />
Pola produksi: jangan kirim objek DB mentah ke client. Map ke DTO kecil di server. Ini bukan soal gaya — ini soal ukuran payload RSC di wire.
3. Caching eksplisit (Next.js 16.4 — Cache Components)
// next.config.ts
import type { NextConfig } from 'next';
const nextConfig: NextConfig = {
cacheComponents: true,
partialPrefetching: true,
};
export default nextConfig;
// app/dashboard/page.tsx
import { Suspense } from 'react';
import { cacheLife, cacheTag } from 'next/cache';
import { db } from '@/lib/db';
export default async function DashboardPage() {
const user = await getCurrentUser(); // request-time, tidak di-cache
return (
<div>
<p>Halo, {user.name}</p>
<Suspense fallback={<Skeleton />}>
<Projects userId={user.id} />
</Suspense>
</div>
);
}
async function Projects({ userId }: { userId: string }) {
'use cache';
cacheLife('hours');
cacheTag(`projects-${userId}`);
const rows = await db.project.findMany({ where: { userId } });
return <ul>{rows.map((p) => <li key={p.id}>{p.name}</li>)}</ul>;
}
Invalidasi setelah mutasi:
'use server';
import { updateTag, revalidateTag } from 'next/cache';
export async function renameProject(id: string, name: string) {
await db.project.update({ where: { id }, data: { name } });
updateTag(`projects-${userId}`); // expire + refresh sekarang (read-your-writes)
}
// revalidateTag(tag, 'max' | 'hours' | 'days' | { expire: 3600 })
// → serve stale, revalidate di background (SWR)
Bedanya: updateTag = blokir sampai fresh (untuk mutasi user sendiri). revalidateTag = background (untuk konten yang boleh sedikit basi).
4. Prefetch selektif — jangan kirim seluruh halaman ke setiap link
// Next.js 16.4: navigation() mengeluarkan konten dari prefetch, tetap dirender saat navigasi nyata
import { navigation } from 'next/cache';
async function Message({ id }: { id: string }) {
const msg = await getMessage(id); // 'use cache' — ikut prefetch
return (
<>
<p>{msg.subject}</p>
<Suspense fallback={<Spinner />}>
<Thread id={id} /> {/* tidak ikut prefetch */}
</Suspense>
</>
);
}
async function Thread({ id }: { id: string }) {
await navigation();
const thread = await getThread(id);
return <>{thread.map((m) => <div key={m.id}>{m.body}</div>)}</>;
}
DIKETAHUI: tiap link dengan prefetch yang resolve URL data = satu server invocation. Di halaman dengan 50 link, itu 50 invocation. navigation() adalah cara resmi memotong biaya itu tanpa mematikan prefetch sepenuhnya.
5. Guard agar route tetap statis
// app/layout.tsx — gagal build kalau ada konten dinamis nyasar
export const ensureStatic = 'navigation'; // 'shell' | 'prefetch' | 'navigation'
Berguna untuk ecommerce/marketing/blog: satu komponen dinamis yang tidak sengaja masuk bisa menaikkan biaya compute seluruh route.
📊 Angka & Benchmark
Yang bisa saya verifikasi dari sumber resmi:
| Angka | Sumber |
|---|---|
Library marked 35.9K (11.2K gz) + sanitize-html 206K (63.3K gz) = ~75K gz yang hilang dari bundle kalau render markdown di Server Component |
react.dev/reference/rsc/server-components |
| Turbopack disk cache Next 16.4: 20–25% lebih kecil (Zstandard untuk data, LZ4 untuk metadata) | nextjs.org/blog/next-16-4 |
| React Compiler (Rust, experimental): memory −30%, compile time −15% | nextjs.org/blog/next-16-4 |
Cache Components jadi default create-next-app sejak 16.4; jadi default penuh di Next 17 |
nextjs.org/blog/next-16-4 |
BELUM PASTI: tidak ada benchmark publik netral (Lighthouse/WebPageTest, beberapa skenario) yang membandingkan RSC vs client-side rendering secara apples-to-apples. Klaim "RSC selalu lebih cepat" tidak bisa saya dukung dengan data. Yang bisa didukung: bundle JS lebih kecil dan tidak ada request waterfall data di client. Itu berbeda dari "LCP lebih cepat" — LCP tergantung TTFB server, yang justru bisa lebih lambat kalau server render jadi bottleneck.
⚖️ Trade-off & Alternatif
| Aspek | RSC (Next App Router) | SSR klasik (Remix/Astro SSR) | SPA + API (Vite/React) |
|---|---|---|---|
| Bundle JS client | Kecil (server code tidak ikut) | Sedang | Besar |
| Data waterfall | Hilang (fetch di server) | Tergantung loader | Ada, harus di-manage |
| Mental model | Kompleks — dua lingkungan, aturan serialisasi | Sedang | Sederhana |
| Caching | Eksplisit ('use cache') tapi banyak knob |
HTTP cache biasa | Client cache (TanStack Query) |
| Biaya server | Per request (kecuali di-cache) | Per request | Nyaris nol |
| Debugging | Susah — stack trace lintas boundary | Sedang | Mudah |
| Lock-in | Tinggi ke Next.js (API bundler non-semver) | Sedang | Rendah |
Trade-off yang paling sering diabaikan: RSC memindahkan biaya dari browser user ke server kamu. Untuk app dengan trafik tinggi dan data yang sering berubah, ini bisa lebih mahal daripada SPA + CDN API. Cache Components ada persis untuk menutup celah ini — tapi caching yang salah = data basi, dan data basi di app finansial lebih mahal daripada bundle 200KB.
Alternatif yang layak dipertimbangkan: Astro (islands, zero-JS default untuk konten), TanStack Start (RSC-like tanpa lock-in Next), atau tetap SPA + TanStack Query kalau app-nya sangat interaktif dan data-heavy (dashboard, editor).
🏭 Use Case Produksi & Failure Mode Umum
Cocok: konten-heavy (blog, docs, ecommerce katalog), dashboard dengan data personal + shell statis, app yang butuh SEO dan first-load cepat.
Kurang cocok: app sangat interaktif (Figma-like, editor realtime), tool internal yang tidak butuh SEO dan timnya kecil — kompleksitas RSC tidak terbayar.
Failure mode yang benar-benar terjadi:
'use client'menyebar ke atas. Satu komponen di root layout diberi'use client', seluruh subtree jadi client. Bundle meledak, RSC-nya sia-sia. Cek dengannext buildoutput ukuran route.- Objek besar menyeberang boundary. Kirim
postutuh (termasukbody50KB) ke client padahal cuma butuhiddanlikes. Payload RSC membengkak, tidak terlihat di bundle analyzer karena bukan JS. - Waterfall di dalam Server Component.
awaitberurutan padahal independen:tsx const user = await getUser(); // 80ms const posts = await getPosts(); // 120ms → total 200ms // ✅ Promise.all → 120msDi client dulu ini kelihatan sebagai spinner; di server ia jadi TTFB. Sama-sama lambat, cuma tidak kelihatan. - State hilang saat navigasi (atau justru tidak hilang). Dengan
cacheComponents: true, Next membungkus route dalam<Activity>— state komponen dipertahankan antar navigasi (maks 3 route). Ini breaking untuk form yang mengharapkan reset. Ada diskusi aktif di repo Next.js soal ini (issue #86577, discussion #89160). DIKETAHUI perilakunya; BELUM PASTI kapan jalur opt-out resmi mendarat. - Cache stampede.
'use cache'dengancacheLife('seconds')di route trafik tinggi → revalidasi bersamaan. Perlu single-flight ataustale-while-revalidate(pakai profil'max'/'hours'kalau data tidak benar-benar butuh detik). Date.now()/Math.random()di dalam'use cache'. Cache Components menolak ini — nilai berbeda tiap eksekusi. Pakaiconnection()untuk memaksa request-time, atau pindahkan ke luar cache scope.
🛠️ Latihan Praktis Kecil
Target: 15 menit, di laptop, tanpa deploy.
npx create-next-app@latest rsc-lab --ts --app --no-tailwind --no-eslint
cd rsc-lab
npm run dev
- Buat
app/heavy/page.tsx:tsx import { marked } from 'marked'; // npm i marked export default async function Page() { const md = '# Halo\n\nIni **server** render.'; return <div dangerouslySetInnerHTML={{ __html: marked.parse(md) as string }} />; } - Buka DevTools → Network → cek bundle JS route
/heavy.markedtidak ada di sana. - Sekarang tambah
'use client'di baris pertama file itu, reload, cek lagi.markedmuncul di bundle. Itu selisih yang dibayar user. - Tambah
console.log('render')di file. Lihat di terminal server (muncul) vs console browser (tidak muncul) → bukti dua lingkungan berbeda. - Coba kirim fungsi sebagai prop ke komponen
'use client'→ baca error persisnya. Hafalkan bunyinya, itu error paling sering muncul.
✅ Cek Pemahaman
- Kamu punya
<ProductCard>yang menampilkan harga dari DB dan tombol "add to cart". Bagian mana yang server, mana yang client, dan data apa persisnya yang menyeberang boundary? - Kenapa
'use client'diapp/layout.tsxmenghapus hampir semua manfaat RSC, padahal hanya satu directive? - Kamu punya route dengan 30 link
prefetch. Tiap prefetch = 1 server invocation. Bagaimana memotong biayanya tanpa mematikan prefetch — dan apa risiko UX-nya?
🔑 Poin Kunci
- RSC = render sebelum bundling, output payload, bukan HTML. Bukan SSR.
'use client'= penanda boundary, bukan "render hanya di client". Semua import di bawahnya ikut bundle.- Aturan serialisasi boundary adalah sumber error #1. Kirim DTO kecil, jangan objek DB utuh.
- Server Component menghapus bundle dan data waterfall — tapi memindahkan biaya ke server. Itu trade-off, bukan kemenangan gratis.
- Next 16.4: caching sekarang opt-in (
'use cache'+cacheLife/cacheTag), default baru untukcreate-next-app, jadi default penuh di Next 17. updateTag= read-your-writes (blokir).revalidateTag= background refresh.refresh()= tidak sentuh cache sama sekali.ensureStatic= guard build-time supaya route tidak diam-diam jadi dinamis.<Activity>menyimpan state antar navigasi (maks 3 route) — ini bisa jadi bug maupun fitur, tergantung app kamu.
➡️ Eksplorasi Lanjut
- React 19.3 stabil:
<ViewTransition>+addTransitionType(animasi enter/exit/share),use(browser())untuk opt-out SSR per komponen, Trusted Types support, dan<Context>bisa dirender langsung dari Server Component tanpa wrapper Provider. - RSC payload wire format — baca spesifikasi Flight di repo
react/react(packages/react-server). Ini yang menentukan ukuran transfer, bukan HTML. - React Compiler — kalau aktif,
useMemo/useCallbackmanual jadi redundan. Cek dulu apakah codebase-mu sudah lolos aturan Rules of React; compiler menolak kode yang melanggar. - Bandingkan dengan TanStack Start — RSC-like tanpa lock-in Next.js, untuk melihat bagian mana dari RSC yang benar-benar esensial vs bagian mana yang framework-specific.
Sumber: [1] React Team. "React 19.3" — react.dev/blog, 9 Sep 2026. https://react.dev/blog/2026/09/09/react-19-3 [2] React Team. "Server Components" — react.dev reference. https://react.dev/reference/rsc/server-components [3] React Team. "React Versions" — react.dev/versions (latest 19.3, patch 19.3.1 14 Okt 2026). https://react.dev/versions [4] Vercel. "Next.js 16.4" — nextjs.org/blog, 6 Okt 2026. https://nextjs.org/blog/next-16-4 [5] Vercel. "Next.js 16" — nextjs.org/blog. https://nextjs.org/blog/next-16 [6] Vercel. "Caching (Cache Components)" — nextjs.org/docs. https://nextjs.org/docs/app/getting-started/caching [7] vercel/next.js. "Multiple issues after migrating to Cache Components" — discussion #89160, 28 Jan 2026. https://github.com/vercel/next.js/discussions/89160
Limitations: Tidak ada benchmark pihak ketiga yang saya verifikasi untuk klaim performa RSC vs alternatif. Semua angka di atas berasal dari blog resmi React/Next.js — yaitu vendor yang menjual solusinya, jadi perlakukan sebagai klaim vendor, bukan pengukuran independen. Perilaku <Activity> saat ini masih bergerak; cek changelog Next.js sebelum mengandalkan workaround apa pun.