Next.js 16: Cache Components - Dari Config Hingga Production - Eps 1

Upgrade yang Bermasalah

Cerita ini pasti familiar bagi banyak developer. Ada yang baru upgrade ke Next.js 16, membaca dokumentasi tentang fitur cache components yang menarik, terus langsung mencoba pakai 'use cache' di component manapun. Hasilnya? Build error muncul atau fitur tidak bekerja sama sekali.

Masalahnya adalah mereka melakukan upgrade tanpa mengaktifkan konfigurasi yang wajib. Di Next.js 16, cacheComponents bukan perilaku default—harus diaktifkan secara eksplisit di next.config.ts. Jika lupa, directive 'use cache' akan diabaikan total oleh compiler. Atau bahkan lebih buruk, terjadi silent fail: tidak ada error tapi fitur juga tidak berfungsi.

Kebingungan yang Sering Terjadi

Ada juga kebingungan serius tentang 'use cache' versus 'use cache: private'. Keduanya terlihat mirip tapi perilakunya sangat berbeda. Jika pakai yang salah, component bisa tidak bisa mengakses cookies atau headers, atau malah tidak sengaja membagikan cache antar pengguna.

Apa yang Akan Dipelajari

Episode ini dirancang untuk membawa developer dari setup paling dasar hingga siap untuk production. Tidak ada jalan pintas atau penyederhanaan berlebihan—hanya praktik yang bekerja dan terbukti.

Pertama, mengapa cacheComponents: true itu wajib di config, bukan hanya opsional. Kedua, perbedaan yang jelas antara tiga varian use cache dan kapan harus menggunakan masing-masing. Ketiga, bagaimana compiler Next.js 16 secara otomatis menghasilkan cache keys tanpa perlu setup manual—ini sangat powerful dan sering terlewatkan oleh developer.

Keempat, Partial Pre-rendering atau PPR—fitur yang memungkinkan menggabungkan konten statis, cached, dan dinamis dalam satu halaman. User melihat konten dengan instan, lalu data dinamis muncul secara progresif di background. Ini benar-benar mengubah game untuk user experience secara keseluruhan.

Kelima, kesalahan umum yang sering terjadi dan cara menghindarinya. Mulai dari lupa mengaktifkan config, cache yang terlalu lama, sampai recursive caching yang bisa membuat performa jelek. Dengan memahami ini, developer bisa percaya diri dalam mengimplementasikan strategi cache mereka.

Ini adalah pondasi sebelum menggali lebih dalam ke strategi cache invalidation di episode selanjutnya. Mari kita mulai mempelajari cara kerja cache components dengan detail dan praktik yang sebenarnya.

Solusi: Satu Baris di Config

Masalahnya adalah satu konfigurasi yang kurang: cacheComponents: true di file next.config.ts. Tanpa ini, directive 'use cache' akan diabaikan sepenuhnya oleh compiler.

Buka file next.config.ts di root project dan tambahkan:

// next.config.ts
const nextConfig = {
  cacheComponents: true,  // MANDATORY
}
export default nextConfig

Jika sudah ada konfigurasi lain, tinggal tambahkan properti ini ke object nextConfig.

Kenapa Flag Ini Wajib

Default behavior Next.js 16 adalah cacheComponents: false. Artinya, cache components tidak aktif sampai developer secara eksplisit mengaktifkannya. Ini berbeda dengan Next.js 15 yang menggunakan unstable_cache dengan implicit behavior.

Pendekatan explicit di Next.js 16 lebih aman karena cache adalah fitur powerful yang bisa menciptakan masalah jika tidak dikelola dengan baik. Dengan membuat flag ini wajib, Next.js memastikan developer benar-benar memahami apa yang mereka aktifkan.

Verifikasi di Production

Jika menggunakan Vercel, platform ini otomatis mendeteksi cacheComponents: true dan mengatur environment-nya. Tidak perlu setup tambahan.

Untuk self-hosted, pastikan build process tidak menunjukkan warning cache-related. Cek juga bahwa file .next/ yang di-generate berisi artifacts cache. Atau lakukan quick test: tambahkan 'use cache' ke satu component dan lihat apakah behavior berubah di production.

Varian Pertama: 'use cache' - Static Caching

image.png

Directive 'use cache' tanpa apa-apa adalah static caching. Component di-render saat build, hasil disimpan, dan dikirim kepada semua pengguna tanpa render ulang. Loading instan karena konten sudah siap.

Trade-off: tidak bisa mengakses cookies, headers, atau request context. Rendering terjadi saat build, bukan saat request—tidak ada data pengguna yang tersedia.

Gunakan ini untuk konten yang identik untuk semua orang. Daftar produk, artikel blog, dokumentasi, FAQ.

'use cache'

export default async function ProductList() {
  const products = await fetch('<https://api.example.com/products>', {
    next: { revalidate: 3600 }
  }).then(res => res.json())

  return (
    <div className="grid">
      {products.map(product => (
        <div key={product.id}>
          <h3>{product.name}</h3>
          <p>${product.price}</p>
        </div>
      ))}
    </div>
  )
}

Varian Kedua: 'use cache: private' - Runtime Data Caching

image.png

'use cache: private' melakukan rendering saat request diterima, bukan saat build. Bisa mengakses cookies dan headers untuk mengidentifikasi pengguna. Cache bersifat per-pengguna—Pengguna A dan Pengguna B tidak berbagi cache.

Requirement penting: durasi cache minimum adalah 30 detik. Durasi lebih pendek membuat caching tidak berguna.

Gunakan ini untuk konten yang spesifik per pengguna. Dashboard personal, rekomendasi, pengaturan.

'use cache: private'

import { cookies } from 'next/headers'

export default async function UserRecommendations() {
  const cookieStore = await cookies()
  const userId = cookieStore.get('userId')?.value

  if (!userId) {
    return <p>Login untuk lihat rekomendasi</p>
  }

  const recommendations = await fetch(
    `https://api.example.com/recommendations/${userId}`,
    {
      next: { cacheLife: '30s' }
    }
  ).then(res => res.json())

  return (
    <div>
      <h2>Rekomendasi Untuk Anda</h2>
      <ul>
        {recommendations.map(item => (
          <li key={item.id}>{item.title}</li>
        ))}
      </ul>
    </div>
  )
}

Varian Ketiga: Granularity Levels

Cache dapat diterapkan di berbagai level: file, component, atau function.

File-level: semua kode dalam file di-cache dengan kebijakan yang sama.

'use cache'

export async function getProductDetails(id: string) {
  return await fetch(`/api/products/${id}`).then(r => r.json())
}

export default function ProductPage({ id }: { id: string }) {
  const product = await getProductDetails(id)
  return <div>{product.name}</div>
}

Component-level: hanya component spesifik tertentu yang di-cache dalam satu file.

export async function StaticHeader() {
  'use cache'
  return <header>...</header>
}

export default async function Page() {
  return <><StaticHeader /><DynamicContent /></>
}

Function-level: hanya function spesifik tertentu yang di-cache.

async function fetchProducts() {
  'use cache'
  return await fetch('/api/products').then(r => r.json())
}

Pilih granularity sesuai kebutuhan. Gunakan file-level untuk seluruh file yang sejenis, component-level untuk beberapa component tertentu, function-level untuk utility spesifik.

Magic: Compiler Menghasilkan Keys Otomatis

Compiler Next.js 16 secara otomatis menghasilkan cache keys berdasarkan route, dynamic segments, dan argumen function. Developer tidak perlu setup key secara manual.

Ini berbeda dengan Next.js 15 yang menggunakan unstable_cache di mana developer harus memberikan key secara eksplisit. Fitur baru ini menghilangkan beban detail teknis dan membuat code lebih clean.

Cara Kerja Compiler

image.png

Compiler mendeteksi struktur route, menganalisis parameter dinamis yang digunakan, kemudian menghasilkan key yang unik dan deterministic. Setiap variasi parameter menghasilkan cache entry terpisah.

Contohnya: route /products/[id] dengan akses /products/1 dan /products/2 akan menghasilkan dua cache keys yang berbeda. Jika user kembali ke /products/1, cache di-serve langsung tanpa render ulang.

Hasil: navigation cepat, server load berkurang, developer tidak perlu menulis logic key generation.

Kapan Menggunakan cacheTag()

Ada situasi di mana auto-generated keys perlu dilengkapi dengan custom behavior. Gunakan cacheTag() untuk menambahkan semantic tags yang memudahkan invalidasi cache secara grouped.

'use cache: private'

import { cacheTag } from 'next/cache'

export default async function ProductDetail({ params }: { params: { id: string } }) {
  cacheTag('products', `product-${params.id}`)

  const product = await fetch(`/api/products/${params.id}`).then(r => r.json())
  const reviews = await fetch(`/api/products/${params.id}/reviews`).then(r => r.json())

  return (
    <div>
      <h1>{product.name}</h1>
      <div>
        {reviews.map(review => (
          <div key={review.id}>
            <p>{review.text}</p>
          </div>
        ))}
      </div>
    </div>
  )
}

Di sini, cache di-tag dengan 'products' dan 'product-${id}'. Saat ada review baru, server bisa invalidate semua entries dengan tag 'products'. Tag ini melengkapi auto-generated keys, bukan menggantikan.

Masalah Lama: Pilih Satu, Tinggal Satu

Sebelum PPR, developer memilih antara render statis untuk kecepatan atau render dinamis untuk data segar. Tidak ada jalan tengah. Statis = instant tapi data stale. Dinamis = fresh data tapi user tunggu.

PPR: Campur Semuanya

image.png

Partial Pre-rendering menggabungkan keduanya dalam satu halaman. Static parts instant, cached parts cepat, dynamic parts di-stream progressif. User lihat sesuatu instantly, lalu konten bertambah di background.

Timeline untuk User

Detik 0: Static shell instant visible. Header, layout, placeholder sudah ready.

Detik 100ms: Cached content muncul. Daftar produk dari cache di-inject tanpa computation.

Detik 500ms+: Dynamic content stream. Real-time data seperti user cart di-update tanpa blocking rendering.

Hasil: halaman terasa instant dengan data tetap segar.

Implementasi

// Static part
export function Header() {
  return <header className="bg-blue-500">...</header>
}

// Cached part
export async function ProductList() {
  'use cache'
  const products = await fetch('/api/products').then(r => r.json())
  return (
    <div>
      {products.map(p => (
        <div key={p.id}>{p.name}</div>
      ))}
    </div>
  )
}

// Dynamic part dengan Suspense
async function UserCart() {
  const cart = await fetch('/api/user/cart', {
    headers: { Authorization: `Bearer ${getToken()}` }
  }).then(r => r.json())

  return <div>Cart: {cart.items.length} items</div>
}

// Combine semua
export default function Page() {
  return (
    <>
      <Header />
      <ProductList />
      <Suspense fallback={<div>Loading cart...</div>}>
        <UserCart />
      </Suspense>
    </>
  )
}

Header instant, ProductList cached, UserCart dynamic. Semua dalam satu halaman tanpa overhead.

Performance Impact

LCP meningkat drastis—dari 2.5 detik jadi 1 detik (57% lebih cepat). User tidak perlu tunggu lama. Background streaming tidak mengganggu karena static dan cached content sudah interactable. Fresh data tetap tersedia untuk dynamic parts tanpa compromise.

Kesalahan 1: Runtime API dalam 'use cache'

'use cache' melakukan rendering saat build, bukan saat request. Tidak bisa mengakses cookies atau headers.

// SALAH
'use cache'
export default async function Dashboard() {
  const cookieStore = await cookies()  // ❌ Error
  return <div>User: {cookieStore.get('userId')?.value}</div>
}

Gunakan 'use cache: private' untuk akses runtime data.

// BENAR
'use cache: private'
export default async function Dashboard() {
  const cookieStore = await cookies()  // ✓ OK
  return <div>User: {cookieStore.get('userId')?.value}</div>
}

Kesalahan 2: Lupa Mengaktifkan cacheComponents: true

Directive cache diabaikan jika config tidak aktif. Build berhasil, tapi cache tidak bekerja.

const nextConfig = {
  cacheComponents: true,  // Wajib ada
}
export default nextConfig

Kesalahan 3: Cache Terlalu Lama

Data stale jika durasi cache terlalu lama. Untuk harga atau inventory, gunakan 5-60 menit, bukan 24 jam.

// SALAH - 24 jam
'use cache'
export async function ProductPrice() {
  const products = await fetch('/api/products', {
    next: { revalidate: 86400 }
  }).then(r => r.json())
  return <div>{products[0].price}</div>
}

// BENAR - 5 menit
'use cache'
export async function ProductPrice() {
  const products = await fetch('/api/products', {
    next: { revalidate: 300 }
  }).then(r => r.json())
  return <div>{products[0].price}</div>
}

Kesalahan 4: Private Cache Tanpa Durasi Minimum

'use cache: private' memerlukan minimum 30 detik cache life. Durasi lebih pendek membuat overhead lebih besar dari benefit.

// SALAH - 5 detik
'use cache: private'
export async function UserData() {
  const data = await fetch('/api/user', {
    next: { cacheLife: '5s' }  // ❌
  }).then(r => r.json())
  return <div>{data.name}</div>
}

// BENAR - 30 detik minimum
'use cache: private'
export async function UserData() {
  const data = await fetch('/api/user', {
    next: { cacheLife: '30s' }  // ✓
  }).then(r => r.json())
  return <div>{data.name}</div>
}

Kesalahan 5: Recursive Caching

Cache di dalam cache membuat logic kompleks dan invalidation rumit. Cache hanya di boundary tertinggi.

// SALAH - Recursive
'use cache'
export async function ProductList() {
  const products = await fetch('/api/products').then(r => r.json())
  return products.map(p => <CachedProductCard product={p} />)
}

'use cache'  // ❌ Jangan cache di sini
export function CachedProductCard({ product }: any) {
  return <div>{product.name}</div>
}

// BENAR - Hanya cache di ProductList
'use cache'
export async function ProductList() {
  const products = await fetch('/api/products').then(r => r.json())
  return products.map(p => <ProductCard product={p} />)
}

export function ProductCard({ product }: any) {  // ✓ Tidak di-cache
  return <div>{product.name}</div>
}

Yang Harus Diingat

Enam poin kunci dari episode ini:

  1. cacheComponents: true di next.config.ts adalah mandatory
  2. 'use cache' untuk static, 'use cache: private' untuk user-specific
  3. Private cache memerlukan minimum 30 detik cache life
  4. Compiler otomatis generate cache keys
  5. PPR = instant UX dengan fresh data
  6. Cache di boundary saja, jangan recursive

Episode Selanjutnya

Episode 2 bahas cache invalidation: updateTag() vs revalidateTag(), strategi invalidasi per use case, dan "read your writes" semantics.

Belajar Lebih Lanjut

Dokumentasi Next.js: nextjs.org/docs

Untuk hands-on practice, pelajari di BuildWithAngga - Next.js 16 Course. Course ini cover foundation hingga advanced invalidation strategies dengan contoh real-world.

Praktik di development sebelum production. Test berbagai scenario: navigation cepat, invalidation, edge cases.