Ringkasan Utama
Cache Components adalah model Next.js 16 di mana data bersifat dinamis secara default dan Anda memilih untuk melakukan caching lewat direktif use cache. cacheLife menentukan berapa lama sebuah entri tetap segar, cacheTag memberinya label, dan revalidateTag menghapusnya sesuai kebutuhan. Model ini juga menjadikan Partial Prerendering sebagai perilaku render default.
Selama bertahun-tahun saya tidak pernah bisa menjawab satu pertanyaan sederhana dengan yakin: apakah halaman ini di-cache? Di App Router, sebuah fetch bisa di-cache secara default, sebuah segmen rute bisa statis, dan satu API dinamis liar bisa diam-diam membuat semuanya menjadi dinamis. Aturannya hidup di default framework, bukan di kode saya, jadi jawaban jujurnya biasanya angkat bahu lalu mengecek langsung di produksi.
Cache Components, model yang aktif lewat satu flag di Next.js 16, memperbaiki hal itu dengan membalik default-nya. Tidak ada yang di-cache kecuali saya menyatakannya. Saya cukup memakai satu direktif, use cache, untuk menandai persis rute, komponen, atau fungsi yang keluarannya ingin saya simpan, dan sisanya tetap dinamis serta di-stream saat permintaan datang. Tulisan ini membahas seluruh permukaannya: direktifnya, profil cacheLife, invalidasi cacheTag dan revalidateTag, flag konfigurasi, dan bagaimana semuanya menyatu ke Partial Prerendering.
Dari default implisit ke opt-in eksplisit
Cerita caching lama bersifat implisit. Permintaan fetch di-cache kecuali Anda memberi opsi no-store, rute penuh diprarender kecuali sesuatu memaksanya dinamis, dan helper seperti unstable_cache menambahkan lapisan ekstra. Hasilnya kuat tetapi sulit dinalar, karena keputusan tentang apa yang di-cache tersebar di antara default, opsi fetch, dan konfigurasi segmen rute, bukan tampak jelas di dalam komponen.
Cache Components membuat keputusan itu eksplisit dan lokal. Pengambilan data bersifat dinamis secara default, dan Anda memilih apa yang di-cache di level halaman, komponen, atau fungsi. Ketika Anda lupa melakukan caching pada sesuatu yang mahal, Anda tetap mendapat keluaran yang benar dan segar, bukan halaman yang diam-diam basi. Konsekuensi yang Anda terima adalah kini Anda menuliskan caching dengan sengaja, dan justru itulah intinya. Berikut perbandingan kedua model tersebut.
| Aspek | Caching implisit (default lama) | use cache eksplisit |
|---|---|---|
| Perilaku default | Data di-cache kecuali Anda menonaktifkannya | Data dinamis kecuali Anda mengaktifkannya |
| Mekanisme opt-in | force-cache, konfigurasi rute, unstable_cache | Satu direktif use cache dalam scope |
| Kendali masa berlaku | Ekspor revalidate dan opsi per-fetch | Profil cacheLife, bernama atau inline |
| Invalidasi | revalidatePath dan revalidateTag | cacheTag plus revalidateTag atau updateTag |
| Di mana keputusan berada | Default framework dan berkas konfigurasi | Terlihat di dalam scope yang di-cache itu sendiri |
Mengaktifkannya dengan flag cacheComponents
Semuanya dimulai dari satu flag. Mengaktifkan cacheComponents di konfigurasi Next.js membuka direktif use cache, fungsi cacheLife, dan cacheTag. Di balik layar, satu flag ini menggantikan tiga opsi eksperimental terpisah yang dulu dikonfigurasi sendiri-sendiri, menyatukan render dinamis, caching, dan prarender menjadi satu pengaturan koheren yang diperkenalkan di Next.js 16.
// next.config.ts
import type { NextConfig } from 'next'
const nextConfig: NextConfig = {
cacheComponents: true,
}
export default nextConfigSetelah flag aktif, use cache bisa berada di tiga level. Di bagian atas berkas ia meng-cache setiap ekspor, dan semua ekspor itu harus berupa fungsi async. Di bagian atas komponen ia meng-cache keluaran render komponen tersebut. Di bagian atas fungsi async mana pun ia meng-cache nilai kembalian fungsi itu. Kunci cache dibangun dari build ID, hash lokasi dan tanda tangan fungsi, serta argumen yang dapat diserialisasi, sehingga input berbeda otomatis mendapat entri berbeda.
Alur caching dan revalidasi nyata
Direktif itu sendiri memakai default yang masuk akal: konten tetap segar di klien selama lima menit dan direvalidasi di server setiap lima belas menit. Sebagian besar waktu saya ingin lebih sengaja, jadi saya memasangkan use cache dengan cacheLife untuk menentukan masa berlaku dan cacheTag untuk memberi label pada entri. Di sini sebuah daftar produk di-cache dengan profil hours dan diberi tag agar bisa saya hapus nanti.
// app/products/page.tsx
import { cacheLife, cacheTag } from 'next/cache'
async function getProducts() {
'use cache'
cacheLife('hours') // stale 5m, revalidate 1h, expire 1d
cacheTag('products') // label this entry for on-demand purges
const res = await fetch('https://api.example.com/products')
return res.json()
}
export default async function ProductsPage() {
const products = await getProducts()
return <ProductGrid products={products} />
}Caching hanya separuh pekerjaan; separuh lainnya adalah invalidasi. Ketika stok berubah, sebuah Server Action memanggil revalidateTag dengan label yang sama. Itu menandai entri bertag tersebut basi dan membangunnya ulang di latar belakang, sehingga pembaca tetap mendapat respons instan sementara salinan segar dibuat. Jika saya butuh pembacaan berikutnya langsung melihat perubahan, saya akan memakai updateTag, yang dibuat untuk skenario baca-tulisan-sendiri.
// app/actions.ts
'use server'
import { revalidateTag } from 'next/cache'
export async function refreshProducts() {
await updateInventory()
// Serve stale, then rebuild the 'products' entry in the background
revalidateTag('products')
}Selalu tetapkan cacheLife eksplisit pada scope yang di-cache. Tanpa itu ia mewarisi revalidate default lima belas menit, dan cache bermasa-hidup pendek yang tersarang bisa diam-diam memperpendek seluruh pohon. Menamai profil membuat siapa pun bisa membaca scope dan tahu kesegarannya sekilas, tanpa menelusuri cache bersarang.
Profil cacheLife: stale, revalidate, expire
Setiap masa berlaku cache adalah tiga angka. Stale adalah berapa lama router klien menyajikan entri tanpa mengecek server. Revalidate adalah seberapa sering server membangunnya ulang di latar belakang, mirip Incremental Static Regeneration. Expire adalah batas keras: setelah selama itu tanpa lalu lintas, permintaan berikutnya menunggu salinan segar. Next.js menyediakan profil bawaan sehingga Anda jarang menyentuh angka mentahnya.
- seconds: stale 30 detik, revalidate 1 detik, expire 1 menit, untuk data real-time seperti skor langsung.
- minutes: stale 5 menit, revalidate 1 menit, expire 1 jam, untuk feed dan berita yang sering diperbarui.
- hours: stale 5 menit, revalidate 1 jam, expire 1 hari, untuk stok atau cuaca yang bergeser sepanjang hari.
- days: stale 5 menit, revalidate 1 hari, expire 1 minggu, untuk artikel dan pos blog.
- max: stale 5 menit, revalidate 30 hari, expire 1 tahun, untuk halaman legal dan konten arsip yang nyaris tak berubah.
Anda juga bisa mendefinisikan profil bernama kustom di konfigurasi, atau memberi objek inline untuk kebutuhan sekali pakai. Properti apa pun yang Anda hilangkan mewarisi dari profil default. Satu catatan penting: profil dengan revalidate nol atau expire di bawah lima menit, termasuk profil seconds, diperlakukan sebagai lubang dinamis dan dikecualikan dari shell yang diprarender alih-alih ditanam di dalamnya.
Invalidasi cacheTag dan revalidateTag
Kedaluwarsa berbasis waktu cocok untuk konten yang terjadwal, tetapi sebagian besar invalidasi digerakkan oleh peristiwa: sebuah produk disunting, sebuah pos diterbitkan, sebuah komentar masuk. cacheTag melekatkan satu atau lebih label string pada entri cache, dan Anda bisa menghasilkan label itu dari datanya sendiri, misalnya memberi tag entri dengan id record. Satu panggilan menerima hingga 128 tag, masing-masing hingga 256 karakter.
Dari sebuah Server Action atau Route Handler Anda lalu memanggil revalidateTag dengan sebuah label untuk menghapus setiap entri yang membawanya. revalidateTag adalah opsi sajikan-basi-sambil-revalidasi: pembaca tetap mendapat salinan lama sampai pembangunan ulang selesai. updateTag adalah saudara yang lebih ketat untuk saat pengguna yang sama yang membuat perubahan harus melihatnya pada pembacaan berikutnya. Keduanya memberi kendali presisi alih-alih menghapus seluruh cache.
Scope yang di-cache berjalan terisolasi dan tidak bisa memanggil cookies, headers, atau membaca searchParams secara langsung; baca semua itu di luar scope dan oper nilainya sebagai argumen. Selain itu, data dinamis yang tak di-cache dan tidak dibungkus dalam batas Suspense akan melempar error saat build, sehingga Anda dipaksa membatasi bagian dinamis dengan sengaja.
Bagaimana kaitannya dengan Partial Prerendering
Cache Components bukan hanya soal data; ia juga model render. Mengaktifkannya menjadikan Partial Prerendering sebagai default, sehingga flag ppr eksperimental lama dan konfigurasi ppr per-rute sudah dihapus. Next.js memprarender sebuah shell HTML statis dari segala sesuatu yang dapat di-cache, menyajikan shell itu seketika, dan men-stream lubang dinamis begitu ia selesai dihitung, semuanya dalam satu rute.
Itulah sebabnya direktif dan batas Suspense bekerja berpasangan. use cache menentukan apa yang masuk ke shell statis; batas Suspense menandai di mana lubang dinamis di-stream dengan sebuah fallback. Anda berhenti memilih antara halaman sepenuhnya statis atau sepenuhnya dinamis dan malah menggabungkan keduanya di pohon yang sama. Bagi saya kemenangan sesungguhnya adalah keterbacaan: batas cache kini hidup di dalam kode, jadi saya akhirnya bisa menjawab apa yang di-cache dengan membaca berkasnya.

