๐ Next.js App Router: Lapisan Caching & Sumber Bug "Data Basi"
Versi referensi: Next.js 16 (rilis Oktober 2025, blog resmi nextjs.org/blog/next-16). Perubahan besar vs 14/15 โ materi ini menyebut dua model: legacy model (tanpa cacheComponents) dan Cache Components (model baru).
๐ฏ Tujuan Belajar
Besok bisa:
- Menjelaskan 4 lapisan cache Next.js App Router dan mana yang menyebabkan "data basi" di kasus tertentu
- Debug laporan "sudah revalidate tapi UI masih lama" dengan checklist per lapisan
- Memakai model baru Next.js 16: use cache, cacheLife, cacheTag, updateTag, refresh
- Memilih API invalidasi yang benar untuk Server Action vs webhook
๐๏ธ Empat Lapisan Caching (diagnosis bug "data basi")
| Lapisan | Lokasi | Di-invalidate oleh | Gejala klasik kalau lupa |
|---|---|---|---|
1. Request Memoization (React cache) |
Dalam 1 render pass | Otomatis per request | โ (bukan sumber basi antar-request) |
2. Data Cache / use cache |
Server, persisten | revalidateTag, revalidatePath, TTL (cacheLife/revalidate) |
Data server basi meski deploy ulang tak selalu (fetch cache bisa lintas deploy) |
| 3. Full Route Cache / prerender | Server (HTML/RSC payload) | revalidatePath, rebuild |
Halaman statis masih versi build lama |
| 4. Client Router Cache | Browser, in-memory | Server Action (revalidateTag/updateTag/refresh), navigasi hard, TTL staleTimes |
Paling sering jadi biang keladi: server sudah fresh, tapi klik <Link> masih render payload lama |
Aturan praktis: keluhan "lihat di halaman A sudah update, halaman B masih lama" atau "refresh dua kali baru berubah" โ lapisan 4 (Client Router Cache) atau kombinasi. Keluhan "bahya request langsung ke server masih lama" โ lapisan 2/3.
Versi 15+ legacy: fetch tidak di-cache default (opt-in via cache: 'force-cache' / next.revalidate / next.tags). Client Router Cache punya staleTimes default: static 5 menit, dynamic 0 detik (eksperimental). Cache entry di Client Router Cache yang di-prefetch bisa basi sampai 5 menit walau server sudah revalidate โ kasus GitHub discussion #86538 (Nov 2025) persis ini, fix: turunkan experimental.staleTimes.static.
๐ป Kode Nyata โ Model Baru Next.js 16
Enable:
// next.config.ts
import type { NextConfig } from 'next'
const nextConfig: NextConfig = {
cacheComponents: true, // 'use cache' dkk. aktif; PPR jadi bagian dari ini
}
export default nextConfig
Cached data function + tag + lifetime:
// lib/posts.ts โ Next.js 16, cacheComponents: true
import { cacheLife, cacheTag } from 'next/cache'
export async function getPost(slug: string) {
'use cache'
cacheLife('days') // stale 5m, revalidate 1d, expire 1w
cacheTag(`post-${slug}`, 'posts')
return db.post.findUnique({ where: { slug } })
}
Preset cacheLife (docs resmi, di-update Sep 2026):
| Profile | stale (client) | revalidate (server) | expire |
|---|---|---|---|
default |
5 menit | 15 menit | never |
seconds |
30 s | 1 s | 1 menit |
minutes |
5 m | 1 m | 1 jam |
hours |
5 m | 1 jam | 1 hari |
days |
5 m | 1 hari | 1 minggu |
max |
5 m | 30 hari | 1 tahun |
Tiga angka ini beda maknanya:
- stale โ boleh serve tanpa cek server (client router pakai x-nextjs-stale-time header; minimum 30 detik di-enforce)
- revalidate โ lewat waktu itu, serve cache + refresh di background (SWR, seperti ISR)
- expire โ lewat waktu itu, request blocking sampai fresh
Invalidasi โ pilih sesuai konteks:
// 1) Server Action, user HARUS langsung lihat hasilnya (read-your-writes)
'use server'
import { updateTag } from 'next/cache'
export async function createPost(formData: FormData) {
const post = await db.post.create({ data: { title: formData.get('title') } })
updateTag('posts') // hanya boleh dari Server Action; request
updateTag(`post-${post.id}`) // berikutnya BLOCKING sampai fresh
}
// 2) Route Handler / webhook CMS โ stale-while-revalidate
// app/api/revalidate/route.ts
import { revalidateTag } from 'next/cache'
export async function POST(req: Request) {
if (req.headers.get('x-secret') !== process.env.REVAL_SECRET) {
return Response.json({ ok: false }, { status: 401 })
}
const { id } = await req.json()
revalidateTag(id, 'max') // Next 16: argumen ke-2 WAJIB
return Response.json({ revalidated: true })
}
// 3) refresh() โ Server Action, hanya refresh data TIDAK di-cache
import { refresh } from 'next/cache'
refresh() // notification count, live metrics โ cache shell tak tersentuh
Catatan penting Next 16:
- revalidateTag(tag) satu argumen โ deprecated. Tanpa argumen kedua behave seperti { expire: 0 }. Pakai revalidateTag(tag, 'max') untuk SWR.
- updateTag hanya valid di Server Action โ dipanggil di Route Handler โ throw. Di webhook pakai revalidateTag(tag, { expire: 0 }) kalau butuh expire instan.
- Dengan cacheComponents: true, export dynamic, revalidate, fetchCache di route segment โ build gagal. Semua caching pindah ke use cache.
- Cache key use cache = build ID + hash fungsi + argumen seriable. Cache tidak lintas deploy (berbeda dengan fetch Data Cache & unstable_cache yang persisten).
- use cache default = in-memory di server. Di serverless (instance ephemeral), entry praktis tak reuse antar-request โ pertimbangkan 'use cache: remote' (cache handler Redis/KV, ada biaya network/platform).
- Runtime API (cookies(), headers(), searchParams) dilarang di dalam use cache โ baca di luar, pass sebagai argumen. Pelanggaran langsung: error next-request-in-use-cache; variasi tak langsung (Promise runtime lewat props/closure/Map bersama) โ build hang 50 detik dengan error "Filling a cache during prerender timed out".
- Versi 16.4+: ensureStatic = 'navigation' untuk memvalidasi route benar-benar statis (pengganti aman dynamic = 'force-static').
Legacy (tanpa cacheComponents) masih didukung โ docs-nya kini dipisah jadi "Caching and Revalidating (Previous Model)". Di model itu: fetch(url, { next: { tags, revalidate } }), export const revalidate = 60, revalidatePath.
๐ Angka & Benchmark
- Client Router Cache default legacy:
staleTimes.static= 300 s,dynamic= 0 s (docsstaleTimes, nextjs.org) - Cache Components:
defaultprofile = stale 5 m / revalidate 15 m / expire never; minimum stale client 30 s (docscacheLife, terakhir di-update 29 Sep 2026) - Timeout prerender
use cache: 50 detik (docs troubleshooting) - Next.js 16 (blog resmi, Okt 2025): Turbopack stable โ klaim build 2โ5ร lebih cepat, Fast Refresh hingga 10ร; >50% dev session di Next 15.3+ sudah pakai Turbopack saat rilis
- Tag cache maksimal 256 karakter (docs
revalidateTag) - DIINFERENSI dari kasus GitHub #86538: sinkronisasi penuh setelah revalidate bisa molor ~15 menit kalau
staleTimes.static300 s + SWR stack โ angka kasus spesifik, bukan universal.
โ๏ธ Trade-off
| Pilihan | Untung | Rugi |
|---|---|---|
Legacy model (fetch + revalidate) |
Stabil, banyak tutorial, cache lintas deploy (unstable_cache/fetch) |
Implicit, dua model API nyampur, sumber kebingungan |
cacheComponents: true |
Eksplisit (opt-in), PPR gratis, build memaksa migrasi segmen lama | Breaking: segmen config dilarang, edge runtime tak didukung, in-memory cache hilang saat deploy |
revalidateTag(tag, 'max') |
Cepat (serve stale), cocok konten toleran delay | User bisa lihat data lama satu request |
updateTag(tag) |
Read-your-writes, zero stale | Hanya Server Action; request berikutnya blocking |
use cache in-memory |
Nol infrastruktur | Serverless: tiap request bisa cache miss; hilang saat deploy |
use cache: remote |
Persisten lintas instance | Network roundtrip, biaya platform, tetap reset pas deploy |
๐ญ Use Case Produksi & Failure Mode
- Webhook CMS publish โ data tetap lama di UI selama menit. Penyebab: webhook cuma
revalidateTag(lapisan 2/3), Client Router Cache di browser user belum expire. Fix: invalidasi dari Server Action pakaiupdateTag, atau terima SWR delay. revalidateTagdi middleware/proxy โ tidak jalan (server context salah). Pindah ke Route Handler atau Server Action.- Build hang 50 s di
use cacheโ Promise runtime (cookies/searchParams) bocor masuk scope cached via props/closure/Map. Refactor: baca di luar, pass nilai primitif. - Serverless +
use cacheโ hit rate nyaris nol, DB kena trafik penuh. Fix:'use cache: remote'atau custom cache handler. - User A update, user A sendiri masih lihat lama โ pakai
updateTag, bukanrevalidateTag(tag, 'max')(SWR sengaja serve stale). - Data beda antar halaman setelah revalidate (kasus nyata discussion #86538, Next 15.5) โ kombinasi fetch cache fresh + Client Router Cache stale. Turunkan
staleTimesatau trigger revalidasi via Server Action.
๐ ๏ธ Latihan Praktis
npx create-next-app@latest cache-lab --ts --app
cd cache-lab
# enable cacheComponents di next.config.ts, lalu:
NEXT_PRIVATE_DEBUG_CACHE=1 npm run dev
- Buat komponen dengan
'use cache'+cacheLife('minutes')+cacheTag('demo'), fetch dari API publik (misal jsonplaceholder). - Bikin Route Handler
POST /api/revalidateyang panggilrevalidateTag('demo', 'max'). - Buka dengan
NEXT_PRIVATE_DEBUG_CACHE=1โ amati log cache hit/miss saat navigasi ulang. - Ubah API data, panggil webhook, lihat: request pertama masih stale, request kedua fresh โ bukti SWR.
- Coba baca
cookies()di dalam scopeuse cacheโ amati errornext-request-in-use-cache, lalu refactor jadi pass-argumen.
โ Cek Pemahaman
- Server log menunjukkan
revalidateTagsudah jalan dan data server fresh, tapi user yang navigasi via<Link>masih melihat konten lama. Lapisan cache mana yang jadi penyebab, dan dua cara memperbaikinya? - Beda perilaku
revalidateTag(tag, 'max')vsupdateTag(tag)pada request yang datang tepat setelah pemanggilan? - Kenapa
use cachedenganstale< 30 detik di-exclude dari prerender/App Shell?
(Jawaban: 1. Client Router Cache; fix via Server Action updateTag/refresh, atau turunkan staleTimes. 2. 'max' = serve stale + revalidate background; updateTag = blocking sampai fresh. 3. Prefetch link akan expire sebelum user sempat klik โ cache entry tak berguna.)
๐ Poin Kunci
- Bug "data basi" hampir selalu bukan satu cache โ diagnose per lapisan: request memoization โ server data cache โ full route cache โ client router cache.
- Next.js 16: caching jadi opt-in eksplisit via
cacheComponents+use cache; segmen config lama (dynamic,revalidate,fetchCache) ditolak build. - Tiga angka
cacheLife:stale(client),revalidate(SWR),expire(blocking). Jangan campur maknanya. - Pilih API invalidasi berdasarkan konteks:
updateTag(Server Action, read-your-writes) /revalidateTag(tag, profile)(webhook, SWR) /refresh()(data uncached). use cachedefault in-memory & reset per deploy โ serverless butuh'use cache: remote'.
โก๏ธ Eksplorasi Lanjutan
use cache: privateโ scope cached yang boleh baca request API (compliance/dedup lintas Cache Function)- Custom
cacheHandlersโ ganti LRU in-memory dengan Redis sendiri cacheTag+ invalidasi granular per-entity (pattern webhook Contentful: tag = entry ID)- Next.js Devtools MCP (baru di 16) โ debugging cache via AI agent
- Migrasi PPR lama: docs "Migrating to Cache Components" + codemod
remove-experimental_ppr
Sumber: nextjs.org/blog/next-16 (Okt 2025) ยท docs use cache, cacheLife, cacheTag, revalidateTag, updateTag, staleTimes, "Migrating to Cache Components", "Caching without Cache Components" (update terakhir AgtโSep 2026) ยท GitHub discussion vercel/next.js #86538 & #91785.