Kenapa ini penting
Bayangkan warung kopi yang menyeduh ulang dari biji setiap kali ada pembeli, padahal 100 orang pesan menu yang sama. Pelanggan menunggu lama dan bahan cepat habis. Caching adalah menyiapkan satu teko kopi untuk banyak pembeli, lalu menyeduh ulang kalau sudah tidak segar. Di web, caching membuat halaman jauh lebih cepat, mengurangi beban database, dan menghemat biaya server. Tapi cache yang salah juga berbahaya: user melihat harga lama, atau data milik user lain. Modul ini membahas model caching Next.js 16 yang berlaku sekarang.
Konsep inti
Dua model caching di Next.js 16
| Cache Components (model baru) | Model lama | |
|---|---|---|
| Aktif kalau | cacheComponents: true di next.config |
cacheComponents tidak diaktifkan |
Proyek baru create-next-app 16.4 |
Ya, default | Tidak |
| Cara meng-cache | "use cache" + cacheLife + cacheTag |
fetch(url, { cache: 'force-cache' }), next: { revalidate } |
| Atur per rute | instant, ensureStatic |
export const revalidate, dynamic, fetchCache, dynamicParams |
Materi ini fokus ke Cache Components karena itu default proyek baru. Opsi model lama seperti export const revalidate = 60 ditolak saat build kalau Cache Components aktif (kami uji: "Route segment config revalidate is not compatible with nextConfig.cacheComponents. Please remove it."). Kamu tetap perlu mengenalnya karena banyak proyek dan tutorial masih memakainya; ringkasannya ada di bagian akhir konsep.
Prinsip dasar Cache Components
Secara default tidak ada yang di-cache. Kamu memilih sendiri apa yang di-cache dengan "use cache". Saat build, Next.js merender setiap rute dan memilah:
- Statis (teks biasa, import modul, perhitungan murni) → masuk "kerangka statis" (static shell) otomatis.
- Di-cache (
"use cache") → hasilnya disimpan dan ikut masuk kerangka statis. - Dinamis (data tanpa cache,
cookies(),headers(),searchParams,connection()) → wajib di dalam<Suspense>; fallback-nya masuk kerangka, isinya di-stream saat request.
Hasil gabungan inilah Partial Prerendering. Kerangka statis bisa disajikan langsung dari CDN.
"use cache" di tiga level
- Level data: di baris pertama fungsi, misalnya
getProducts(). Hasil fungsi di-cache. - Level UI: di baris pertama komponen, page, atau layout. Hasil render di-cache.
- Level file: di baris pertama file. Semua fungsi yang di-export di file itu di-cache.
Argumen fungsi otomatis menjadi bagian kunci cache. getPost('a') dan getPost('b') disimpan terpisah. Argumen dan nilai yang dikembalikan harus bisa diserialisasi.
Di dalam "use cache" kamu tidak boleh memanggil cookies() atau headers() langsung. Baca nilainya di luar (di komponen dalam Suspense), lalu kirim sebagai argumen ke fungsi cache. Alternatifnya ada varian "use cache: private" (untuk data per user) dan "use cache: remote" (cache bersama yang tahan lama, misalnya di serverless); keduanya topik lanjutan.
cacheLife: berapa lama data dianggap segar
Pasang cacheLife di setiap "use cache". Tiga angka yang diatur:
- stale: berapa lama browser boleh memakai data tanpa bertanya ke server.
- revalidate: setelah ini, request berikutnya masih mendapat data lama, sementara server membuat versi baru di belakang layar (stale-while-revalidate).
- expire: kalau tidak ada request selama ini, data dibuang, dan request berikutnya menunggu data baru.
| Profil | stale | revalidate | expire |
|---|---|---|---|
default |
5 menit | 15 menit | tidak pernah |
seconds |
30 detik | 1 detik | 60 detik |
minutes |
5 menit | 1 menit | 1 jam |
hours |
5 menit | 1 jam | 1 hari |
days |
5 menit | 1 hari | 1 minggu |
weeks |
5 menit | 1 minggu | 30 hari |
max |
5 menit | 30 hari | 1 tahun |
Bisa juga objek: cacheLife({ stale: 60, revalidate: 300, expire: 3600 }) (dalam detik), atau profil sendiri di next.config lewat opsi cacheLife. Cache yang sangat pendek (profil seconds, atau expire di bawah 5 menit) tidak dimasukkan ke kerangka statis dan diperlakukan sebagai bagian dinamis.
cacheTag dan revalidasi on-demand
Revalidasi berbasis waktu cocok untuk data yang berubah rutin. Untuk data yang berubah karena aksi (admin menerbitkan artikel), lebih baik memberi label dengan cacheTag('posts'), lalu membuang cache berlabel itu saat data berubah:
| Fungsi | Di mana boleh dipanggil | Perilaku | Pakai untuk |
|---|---|---|---|
updateTag('posts') |
Hanya Server Action | Cache langsung kedaluwarsa; user yang mengubah langsung melihat hasilnya | "Saya simpan, saya lihat hasilnya" |
revalidateTag('posts', 'max') |
Server Action dan Route Handler | Ditandai basi; pengunjung berikutnya masih dapat versi lama sementara versi baru dibuat | Webhook CMS, update latar |
revalidatePath('/blog') |
Server Action dan Route Handler | Buang cache untuk satu path | Saat tidak tahu tag-nya |
Perhatikan: di Next.js 16, revalidateTag wajib dua argumen. Bentuk lama revalidateTag('posts') sudah deprecated. Argumen kedua biasanya 'max'. Dokumentasi juga menyarankan revalidasi berbasis tag dibanding path karena lebih presisi.
Server Action adalah fungsi async di file berawalan 'use server' yang bisa dipanggil langsung dari form (<form action={fungsi}>). Fungsi ini selalu berjalan di server. Pembahasan lengkap ada di materi berikutnya; di sini kita pakai untuk memicu revalidasi.
Statis vs dinamis di laporan build
npm run build menampilkan simbol di setiap rute:
○Static: sepenuhnya dibuat saat build.●SSG: dibuat saat build darigenerateStaticParams.◐Partial Prerender: kerangka statis + bagian dinamis yang di-stream.- Kolom Revalidate/Expire muncul untuk rute yang memakai cache (misalnya
1h 1duntukcacheLife('hours')).
ISR di dunia Cache Components
ISR (Incremental Static Regeneration, halaman statis yang diperbarui berkala) sekarang tidak butuh konfigurasi khusus. Kombinasi generateStaticParams + fungsi data dengan "use cache" + cacheLife('hours') menghasilkan halaman statis yang diperbarui tiap jam. Slug baru yang belum ada saat build dirender saat pertama dikunjungi lalu disimpan.
Ringkasan model lama (proyek tanpa Cache Components)
fetch(url, { cache: 'force-cache' })meng-cache sebuah request.fetch(url, { next: { revalidate: 3600 } })cache dengan revalidasi tiap jam.fetch(url, { next: { tags: ['posts'] } })memberi tag.export const revalidate = 3600di page/layout mengatur revalidasi seluruh rute.export const dynamic = 'force-dynamic'memaksa rute dirender tiap request.
Kalau kamu memelihara proyek seperti ini, dokumentasinya ada di panduan "Caching and Revalidating (Previous Model)".
Contoh kode
Cache level data dengan profil dan tag:
// lib/posts.js
import { cacheLife, cacheTag } from 'next/cache'
export async function getPosts() {
'use cache' // baris PERTAMA di badan fungsi
cacheLife('hours') // segar 1 jam, dibuang setelah 1 hari tanpa request
cacheTag('posts') // label untuk revalidasi on-demand
const res = await fetch('https://api.kopiku.id/posts')
return res.json()
}
export async function getPost(slug) {
'use cache'
cacheLife('hours')
cacheTag('posts', `post-${slug}`) // boleh lebih dari satu tag
const res = await fetch(`https://api.kopiku.id/posts/${slug}`)
return res.json()
}
// → getPost('a') dan getPost('b') disimpan sebagai entri cache terpisah (argumen = kunci)
Cache level UI: satu komponen di-cache, bagian personal di-stream:
// app/beranda/page.jsx
import { Suspense } from 'react'
import { cookies } from 'next/headers'
import { cacheLife } from 'next/cache'
async function Promo() {
'use cache'
cacheLife('days') // promo sama untuk semua orang, diperbarui harian
const res = await fetch('https://api.kopiku.id/promo')
const promo = await res.json()
return <p className="rounded bg-amber-100 p-3">{promo.teks}</p>
}
async function Sapaan() {
const nama = (await cookies()).get('nama')?.value ?? 'Tamu' // data per user
return <p>Halo, {nama}!</p>
}
export default function BerandaPage() {
return (
<main className="space-y-4 p-6">
<h1 className="text-2xl font-bold">Kopiku</h1> {/* statis */}
<Promo /> {/* di-cache, masuk kerangka */}
<Suspense fallback={<p>Halo!</p>}>
<Sapaan /> {/* di-stream saat request */}
</Suspense>
</main>
)
}
Server Action yang memicu revalidasi:
// app/actions.js
'use server'
import { updateTag, revalidateTag, revalidatePath } from 'next/cache'
export async function terbitkanArtikel(formData) {
const judul = formData.get('judul')
// await db.post.create({ data: { judul } }) ← simpan ke database
console.log('Simpan ke database:', judul)
updateTag('posts') // penulis langsung melihat artikel barunya
}
export async function segarkanDiLatar() {
revalidateTag('posts', 'max') // WAJIB argumen kedua di Next.js 16
}
export async function segarkanHalamanBlog() {
revalidatePath('/blog')
}
// app/tulis/page.jsx
import { terbitkanArtikel } from '../actions'
export default function TulisPage() {
return (
<form action={terbitkanArtikel} className="space-y-2 p-6">
<input name="judul" placeholder="Judul artikel" className="border px-2" />
<button type="submit" className="rounded bg-black px-3 py-1 text-white">Terbitkan</button>
</form>
)
}
// → klik Terbitkan → fungsi jalan di server → cache bertag 'posts' kedaluwarsa
Route Handler untuk webhook CMS (misalnya dipanggil saat editor menekan "Publish"):
// app/api/revalidate/route.js
import { revalidateTag } from 'next/cache'
export async function POST(request) {
const rahasia = request.headers.get('x-rahasia')
if (rahasia !== process.env.REVALIDATE_SECRET) {
return Response.json({ ok: false }, { status: 401 })
}
revalidateTag('posts', 'max') // updateTag tidak boleh di sini (khusus Server Action)
return Response.json({ ok: true })
}
// → curl -X POST -H "x-rahasia: ..." https://kopiku.id/api/revalidate → {"ok":true}
Profil cache sendiri di konfigurasi:
// next.config.mjs
const nextConfig = {
cacheComponents: true,
partialPrefetching: true,
cacheLife: {
katalog: {
stale: 300, // 5 menit
revalidate: 900, // 15 menit
expire: 86400, // 1 hari
},
},
}
export default nextConfig
// → di fungsi: cacheLife('katalog')
Nilai acak atau waktu sekarang harus dipilih eksplisit: per request atau di-cache.
import { Suspense } from 'react'
import { connection } from 'next/server'
async function IdUnik() {
await connection() // beda di setiap request
return <p>ID request: {crypto.randomUUID()}</p>
}
async function IdBuild() {
'use cache' // sama untuk semua orang sampai cache diperbarui
return <p>ID build: {crypto.randomUUID()}</p>
}
export default function Page() {
return (
<>
<IdBuild />
<Suspense fallback={<p>...</p>}><IdUnik /></Suspense>
</>
)
}
Kesalahan umum
- Memakai
export const revalidateataudynamic = 'force-dynamic'di proyek Cache Components. Build menolak. Pakai"use cache"+cacheLife, atauconnection()+<Suspense>untuk data per request. revalidateTag('posts')dengan satu argumen. Deprecated di Next.js 16. TulisrevalidateTag('posts', 'max'), atauupdateTag('posts')di Server Action.- Memanggil
cookies()di dalam fungsi"use cache". Tidak diizinkan, dan berbahaya: data satu user bisa tersaji ke user lain. Baca cookie di luar, kirim nilainya sebagai argumen. - Memanggil
updateTagdi Route Handler.updateTagkhusus Server Action. Di Route Handler pakairevalidateTag. - Mengandalkan cache in-memory di serverless. Di lingkungan serverless, entri cache runtime bisa hilang antar request. Untuk cache bersama yang tahan lama, gunakan
"use cache: remote"dengan penyimpanan yang dikonfigurasi platform.
Latihan
- Mudah, Buat fungsi
getKurs()yang mengambilhttps://open.er-api.com/v6/latest/USD, di-cache dengan profilhoursdan tagkurs. Tampilkan kurs IDR di halaman/kurs. - Sedang, Tambahkan tombol "Perbarui kurs" di halaman
/kursberupa form yang memanggil Server Action berisiupdateTag('kurs'). - Menantang, Buat halaman
/menuyang menampilkan daftar menu (di-cachedays, tagmenu), sapaan berdasarkan cookienama(di-stream), dan jam server (per request). Pastikannpm run buildlolos dan rutenya bertanda◐.
Lihat solusi
1.
// lib/kurs.js
import { cacheLife, cacheTag } from 'next/cache'
export async function getKurs() {
'use cache'
cacheLife('hours')
cacheTag('kurs')
const res = await fetch('https://open.er-api.com/v6/latest/USD')
const data = await res.json()
return data.rates.IDR
}
// app/kurs/page.jsx
import { getKurs } from '@/lib/kurs'
export default async function KursPage() {
const idr = await getKurs() // di-cache → tidak perlu Suspense
return <p className="p-6 text-xl">1 USD = Rp{Math.round(idr).toLocaleString('id-ID')}</p>
}
2.
// app/kurs/actions.js
'use server'
import { updateTag } from 'next/cache'
export async function perbaruiKurs() {
updateTag('kurs')
}
// app/kurs/page.jsx
import { getKurs } from '@/lib/kurs'
import { perbaruiKurs } from './actions'
export default async function KursPage() {
const idr = await getKurs()
return (
<main className="space-y-3 p-6">
<p className="text-xl">1 USD = Rp{Math.round(idr).toLocaleString('id-ID')}</p>
<form action={perbaruiKurs}>
<button className="rounded border px-3 py-1">Perbarui kurs</button>
</form>
</main>
)
}
3.
// app/menu/page.jsx
import { Suspense } from 'react'
import { cookies } from 'next/headers'
import { connection } from 'next/server'
import { cacheLife, cacheTag } from 'next/cache'
async function DaftarMenu() {
'use cache'
cacheLife('days')
cacheTag('menu')
const menu = ['Kopi Susu', 'Americano', 'Teh Tarik'] // di proyek nyata: query database
return <ul>{menu.map((m) => <li key={m}>{m}</li>)}</ul>
}
async function Sapaan() {
const nama = (await cookies()).get('nama')?.value ?? 'Tamu'
return <p>Selamat datang, {nama}!</p>
}
async function Jam() {
await connection()
return <p>Jam server: {new Date().toLocaleTimeString('id-ID')}</p>
}
export default function MenuPage() {
return (
<main className="space-y-4 p-6">
<h1 className="text-2xl font-bold">Menu</h1>
<DaftarMenu />
<Suspense fallback={<p>Selamat datang!</p>}><Sapaan /></Suspense>
<Suspense fallback={<p>Jam server: ...</p>}><Jam /></Suspense>
</main>
)
}
// → npm run build: ├ ◐ /menu
Ringkasan
- Next.js 16 punya dua model; proyek baru memakai Cache Components (
cacheComponents: true), dan opsi lama sepertiexport const revalidateditolak di model ini. - Tidak ada yang di-cache secara default; pilih sendiri dengan
"use cache"di level fungsi, komponen, atau file. Argumen menjadi kunci cache. cacheLifemengatur stale/revalidate/expire lewat profil (hours,days,max, dll.) atau objek.cacheTag+updateTag(Server Action, langsung),revalidateTag(tag, 'max')(stale-while-revalidate), ataurevalidatePathuntuk revalidasi on-demand.- Laporan build menunjukkan
○statis,●SSG,◐Partial Prerender; ISR =generateStaticParams+"use cache"+cacheLife.