Skip to main content
BlogWeb Dev

Cara Kerja Caching Next.js: Static, Dynamic, dan Revalidate

Panduan pemula cara kerja caching Next.js static dan dynamic: baca simbol output build, generateStaticParams, revalidate, lalu cek header dengan curl.

Matthews Wong

Software Engineer

Oktober 202610 menit baca
Cara Kerja Caching Next.js: Static, Dynamic, dan Revalidate

Ringkasan Utama

Cara kerja caching Next.js: route dirender statis, yaitu HTML dibuat saat build dan dipakai semua pengunjung, selama kodenya tidak membaca cookies, headers, searchParams, atau data per request. Baca simbol ○, ● dan ƒ di output next build, daftarkan path lewat generateStaticParams, segarkan dengan revalidate, lalu buktikan lewat curl di server produksi.

Kamu mengubah isi sebuah halaman, deploy, lalu halaman itu masih menampilkan data lama. Di lain waktu kebalikannya: halaman yang kamu kira statis ternyata dirender ulang di setiap kunjungan, atau waktu yang tampil di halaman tidak pernah berubah. Semuanya bermuara pada satu pertanyaan: route ini dirender saat build, atau saat ada request?

Panduan ini membangun empat route kecil di Next.js 15 (App Router) dan memeriksa tiap route dengan dua alat yang selalu ada: output next build dan header respons dari curl. Setiap opsi dicocokkan dengan dokumentasi Next.js di bagian sumber. Fungsi penghitung header dan simbol di output build saya baca langsung di kode Next.js 15.5.7 yang terpasang di proyek situs ini, dan di halaman post blog situs ini saya memang melihat header x-nextjs-cache: HIT dan cache-control: s-maxage=31536000.

Mau paham konsepnya lewat simulasi?

Panduan ini fokus pada langkah dan error. Lesson gratis kami menjelaskan alurnya dari build sampai halaman segar lewat animasi, plus kuis tentang apa yang membuat route dinamis dan apa default fetch di Next.js 15: Rendering Statis vs Dinamis di Next.js: Caching Dijelaskan. Kerjakan panduan ini dulu supaya kamu punya empat route yang bisa diperiksa, lalu buka lesson untuk melihat bagaimana situs ini memakainya.

Yang kamu butuhkan

Semuanya berjalan di laptop, tanpa server dan tanpa layanan tambahan.

  • Proyek Next.js 15 dengan App Router dan TypeScript, misalnya hasil create-next-app.
  • Dua terminal. Satu menjalankan npm run build lalu npm start, satu lagi untuk curl. Semua pengujian caching di sini memakai server produksi, bukan npm run dev.
  • curl di terminal, karena yang kita periksa adalah isi HTML dan header respons, bukan tampilan di browser.
  • Tidak perlu database atau API eksternal. Semua contoh memakai waktu dan angka buatan sendiri supaya hasilnya bisa diprediksi.

Langkah 1: Buat halaman statis dan baca simbol di output build

Mulai dari halaman yang jelas statis. Halaman ini menampilkan waktu saat ia dirender. Jika benar dirender sekali saat build, waktunya akan membeku. Ujilah di mode produksi, karena menurut dokumentasi, saat development halaman selalu dirender saat diminta dan tidak pernah di-cache.

// app/about/page.tsx
// Reads no cookies, headers or searchParams, so Next.js renders it at build time.
export default function AboutPage() {
  // Computed ONCE, during next build. Every visitor gets the same HTML.
  const builtAt = new Date().toISOString();
  return (
    <main>
      <h1>About</h1>
      <p>
        Rendered at: <time>{builtAt}</time>
      </p>
    </main>
  );
}

// Terminal 1 (production mode, NOT npm run dev):
//   npm run build
//   npm start
//
// Terminal 2: run it twice and compare.
//   curl -s http://localhost:3000/about | grep -o "<time>[^<]*</time>"
//   curl -s http://localhost:3000/about | grep -o "<time>[^<]*</time>"

Output yang diharapkan: next build mencetak daftar route dengan satu simbol di depan tiap baris, dan /about bertanda ○ (Static), yang berarti prerendered as static content. Dua curl berturut-turut mengembalikan isi time yang persis sama, yaitu waktu build, bukan waktu request.

Hanya ada tiga simbol yang perlu kamu hafal. ○ (Static): HTML dibuat saat build. ● (SSG): HTML statis yang dibuat dari generateStaticParams. ƒ (Dynamic): dirender di server setiap ada request. Legendanya dicetak di bawah daftar route, jadi kamu tidak perlu menebak. Simbol keempat, ◐ (Partial Prerender), hanya muncul jika Partial Prerendering aktif, dan itu di luar panduan ini.

Langkah 2: Buat route dinamis dan lihat apa yang mengubahnya

Sekarang route yang memang harus dirender per request. Cara paling jujur adalah await connection() dari next/server, yang memberi tahu Next.js bahwa render harus menunggu request masuk. Dokumentasi memakainya untuk nilai seperti new Date() atau Math.random(). Hal lain yang berefek sama: cookies, headers, draftMode, prop searchParams, unstable_noStore, dan fetch dengan cache no-store. Satu saja yang dibaca, route-nya dinamis.

// app/now/page.tsx
import { connection } from "next/server";

export default async function NowPage() {
  // Wait for a real request before rendering anything below this line.
  await connection();
  const now = new Date().toISOString(); // computed per request
  return (
    <main>
      <h1>Now</h1>
      <p>
        Rendered at: <time>{now}</time>
      </p>
    </main>
  );
}

// Wrong: adding this to app/about/page.tsx "just to read a cookie".
//   import { cookies } from "next/headers";
//   const session = (await cookies()).get("session"); // Next.js 15: cookies() is async
// The whole /about route silently becomes dynamic.
// Right: keep /about free of request data, and read the cookie only in the
// route that really needs it.

//   npm run build   -> /now is marked as dynamic, /about stays static
//   npm start
//   curl -s http://localhost:3000/now | grep -o "<time>[^<]*</time>"
//   curl -s http://localhost:3000/now | grep -o "<time>[^<]*</time>"

Output yang diharapkan: Build menandai /now dengan ƒ (Dynamic), sedangkan /about tetap ○. Setiap curl ke /now mengembalikan waktu yang berbeda, karena HTML dibuat ulang di setiap request.

Jebakan terbesar pemula: satu baris cookies() atau satu prop searchParams di halaman statis mengubah seluruh route dari ○ menjadi ƒ, tanpa pesan error apa pun. Setiap kali menambahkan kode yang membaca request, jalankan next build lagi dan lihat simbolnya.

Langkah 3: Prerender route berparameter dengan generateStaticParams

Route seperti /produk/[id] adalah satu template dengan banyak path. generateStaticParams mengembalikan array objek, satu per path, dan Next.js membuat halaman-halaman itu saat build, bukan saat diminta. Di Next.js 15, params adalah Promise, jadi kamu menulis await params. Lalu dynamicParams: false membuat path yang tidak terdaftar mengembalikan 404. Defaultnya, true, merender path itu saat pertama kali diminta.

// app/produk/[id]/page.tsx

// Any id not returned below answers 404 instead of rendering on demand.
export const dynamicParams = false;

// Runs once, at build time, before the page is generated.
export function generateStaticParams() {
  return [{ id: "1" }, { id: "2" }, { id: "3" }];
}

export default async function ProdukPage({
  params,
}: {
  params: Promise<{ id: string }>;
}) {
  // Next.js 15: params is a Promise.
  const { id } = await params;
  const builtAt = new Date().toISOString();
  return (
    <main>
      <h1>Produk {id}</h1>
      <p>
        Rendered at: <time>{builtAt}</time>
      </p>
    </main>
  );
}

//   npm run build   -> /produk/[id] is marked as SSG, with paths listed under it
//   npm start
//   curl -s -o /dev/null -w "%{http_code}\n" http://localhost:3000/produk/1
//   curl -s -o /dev/null -w "%{http_code}\n" http://localhost:3000/produk/99

Output yang diharapkan: Build menandai /produk/[id] dengan ● (SSG), dengan beberapa path di bawahnya. Setelah npm start, /produk/1 menjawab 200 dan /produk/99 menjawab 404.

Satu aturan lagi: generateStaticParams harus selalu mengembalikan array, termasuk array kosong. Array kosong berarti tidak ada path yang dibuat saat build, tetapi setiap path dirender saat pertama dikunjungi lalu disimpan. Tidak mengembalikan apa pun membuat route menjadi dinamis.

Langkah 4: Tambahkan revalidate agar halaman statis tidak membeku

Halaman statis bisa jadi basi. export const revalidate = 60 berarti: setelah 60 detik, request berikutnya tetap menerima halaman lama, sementara versi baru dibuat di latar belakang, dan request sesudahnya menerima versi baru. Polanya disebut stale-while-revalidate. Dokumentasi menyarankan nilai yang tinggi, misalnya satu jam, dan revalidatePath atau revalidateTag jika kamu butuh presisi.

// app/produk/[id]/page.tsx  (add one line near the top)

// Wrong: export const revalidate = 60 * 10;   (not statically analysable)
// Right: a plain number, in seconds.
export const revalidate = 60;

// Rebuild, restart, then run this in terminal 2:
//   for i in 1 2; do
//     curl -s http://localhost:3000/produk/1 | grep -o "<time>[^<]*</time>"
//   done
//   sleep 65
//   curl -s http://localhost:3000/produk/1 | grep -o "<time>[^<]*</time>"  # still the OLD time; regeneration starts
//   sleep 3
//   curl -s http://localhost:3000/produk/1 | grep -o "<time>[^<]*</time>"  # the NEW time

Output yang diharapkan: Dua curl pertama menampilkan waktu yang sama. Request pertama setelah lewat 60 detik masih menampilkan waktu lama, dan request sesudahnya menampilkan waktu baru. Jika masih menampilkan waktu lama, tunggu sebentar lalu ulangi: pembuatan versi baru berjalan di latar belakang.

Nilai revalidate harus bisa dianalisis statis: 600 valid, 60 * 10 tidak. Dan revalidate terendah di antara layout, page, dan fetch pada satu route menentukan frekuensi seluruh route.

Langkah 5: Atur fetch dan route handler dengan sengaja

Dua default berubah di Next.js 15 dan membuat banyak tutorial lama menyesatkan. fetch tidak di-cache secara default, dan Route Handler GET juga tidak. Aktifkan sesuai kebutuhan: cache force-cache atau next.revalidate untuk fetch, dan dynamic force-static untuk handler. fetch dengan no-store, atau revalidate 0, membuat route menjadi dinamis.

// Inside any async Server Component. The URL is a placeholder.

// Wrong (a Next.js 14 habit): expecting this to be cached.
const a = await fetch("https://api.example.com/produk");

// Right: opt in on the call.
const b = await fetch("https://api.example.com/produk", { cache: "force-cache" });
const c = await fetch("https://api.example.com/produk", { next: { revalidate: 3600 } });

// This one makes the route dynamic.
const d = await fetch("https://api.example.com/produk", { cache: "no-store" });

// app/api/waktu/route.ts
// A GET handler is NOT cached by default in Next.js 15. This one has no
// per-request input, so opt back in. Delete the line to see it become dynamic.
export const dynamic = "force-static";

export function GET() {
  return Response.json({ builtAt: new Date().toISOString() });
}

//   curl -s http://localhost:3000/api/waktu
//   curl -s http://localhost:3000/api/waktu

Output yang diharapkan: Setelah force-static ditambahkan, /api/waktu tampil sebagai ○ di output build dan dua curl mengembalikan builtAt yang sama. Hapus barisnya dan route itu menjadi ƒ.

Dua catatan. Pertama, route yang berisi fetch tanpa opsi tetap bisa diprerender dan HTML-nya di-cache; jika kamu harus menjamin route dinamis, pakai connection(). Kedua, semua ini berlaku untuk Next.js 15. Next.js 16 memperkenalkan model Cache Components dengan use cache, jadi cek versi yang dibahas sebuah tutorial sebelum menyalin konfigurasinya. Perbandingannya ada di artikel Cache Components dan use cache.

Langkah 6: Buktikan dengan header respons

Simbol di build menjawab apa yang Next.js rencanakan. Header menjawab apa yang benar-benar keluar. Di Next.js 15.5.7, fungsi getCacheControlHeader menghasilkan s-maxage sebesar nilai revalidate, s-maxage satu tahun (31536000 detik) jika revalidate tidak diatur, dan private, no-cache, no-store, max-age=0, must-revalidate jika revalidate bernilai 0.

# -I asks for the headers only. grep keeps the lines that matter.
curl -sI http://localhost:3000/about | grep -iE "^(HTTP|cache-control|x-nextjs)"
curl -sI http://localhost:3000/produk/1 | grep -iE "^(HTTP|cache-control|x-nextjs)"

# The rule behind the numbers, from getCacheControlHeader in Next.js 15.5.7:
#   revalidate = 60      -> cache-control starts with s-maxage=60
#   revalidate not set   -> s-maxage=31536000 (one year)
#   revalidate = 0       -> private, no-cache, no-store, max-age=0, must-revalidate

Output yang diharapkan: /about membawa s-maxage=31536000, dan /produk/1 membawa s-maxage=60 di awal nilainya. Header x-nextjs-cache dan x-nextjs-prerender tidak ada di dokumentasi resmi, tetapi muncul di halaman prerender Next.js 15.5.7 (di halaman blog situs ini saya melihat HIT dan 1). Anggap keduanya petunjuk debugging, bukan kontrak.

Cache-Control mengatur apakah browser atau CDN bertanya ke server sama sekali, sedangkan cache render mengatur apakah server merender ulang. Keduanya lapisan yang terpisah. Jika kamu mengatur Cache-Control sendiri di next.config, Next.js 15 tidak lagi menimpanya dengan default miliknya, jadi header buatanmulah yang keluar.

Tambahkan NEXT_PRIVATE_DEBUG_CACHE=1 ke file .env sebelum npm start. Menurut dokumentasi ISR, konsol server lalu mencatat cache hit dan miss, sehingga kamu bisa melihat halaman mana yang dibuat saat build dan mana yang diperbarui saat diakses.

Error umum dan cara memperbaikinya

Hampir semua masalah caching di Next.js jatuh ke delapan gejala ini. Cocokkan gejalamu dengan kolom pertama.

GejalaPenyebabPerbaikan
Halaman yang kamu kira statis ternyata ƒ di output buildRoute membaca cookies, headers, searchParams, connection, atau fetch dengan no-store.Cari API yang disentuh, pindahkan ke route lain atau jauhkan dari route itu, lalu build ulang dan cek simbolnya.
Data sudah berubah, halaman tetap menampilkan yang lamaHalaman diprerender dan belum ada revalidate, atau jendelanya belum lewat. Request pertama setelah kedaluwarsa memang masih menerima versi lama.Atur revalidate, atau panggil revalidatePath atau revalidateTag setelah data berubah. Refresh dua kali sebelum menyimpulkan.
Kamu menguji revalidate di npm run dev dan tidak terjadi apa-apaSaat development, halaman selalu dirender saat diminta dan tidak pernah di-cache.Jalankan npm run build lalu npm start, dan uji di sana.
Waktu atau angka acak di halaman tidak pernah berubahNilainya dihitung saat build di route statis, lalu HTML-nya dipakai berulang.Tambahkan await connection() agar dirender per request, atau pakai revalidate bila cukup diperbarui berkala.
Path yang valid mengembalikan 404 di server produksidynamicParams bernilai false dan path itu tidak ada di generateStaticParams.Tambahkan path ke daftar, atau hapus dynamicParams agar path baru dirender saat pertama diminta.
Halaman jadi lebih lambat setelah upgrade ke Next.js 15fetch tidak lagi di-cache secara default, jadi setiap render memanggil ulang sumber datanya.Tambahkan cache force-cache atau next revalidate pada pemanggilan fetch yang datanya boleh dipakai ulang.
Route handler GET tampil sebagai ƒ padahal isinya tidak pernah berubahDi Next.js 15, handler GET tidak di-cache secara default.Tambahkan export const dynamic = force-static pada file route.ts.
revalidate = 60 * 10 tidak bekerja seperti yang kamu maksudNilai revalidate harus bisa dianalisis statis, dan dokumentasi menyebut ekspresi seperti itu tidak valid.Tulis angka literal: revalidate = 600.

Checklist sebelum kamu menganggap caching beres

  • Kamu menguji di npm run build dan npm start, bukan npm run dev.
  • Setiap route di output build punya simbol yang kamu harapkan: ○, ● atau ƒ.
  • Route berparameter punya generateStaticParams, dan dynamicParams sengaja kamu pilih.
  • Halaman yang datanya berubah punya revalidate berupa angka literal, atau dipicu revalidatePath dan revalidateTag.
  • fetch dan Route Handler GET yang boleh di-cache sudah kamu aktifkan secara eksplisit.
  • Kamu membaca header Cache-Control dengan curl -I, bukan menebaknya.

Belajar lebih dalam

Panduan ini mengajarkan cara memeriksa. Lesson gratis menjelaskan alasannya: simulasi dari generateStaticParams sampai regenerasi di latar belakang, contoh kode nyata dari situs ini (layout blog, layout locale dengan dynamicParams false, halaman open-source dengan revalidate), dan kuis tiga soal.

Buka lesson: Rendering Statis vs Dinamis di Next.js

Setelah route-mu statis dengan benar, lanjutkan ke optimasi lain di panduan optimasi performa Next.js.

FAQ

Pertanyaan Umum

Jawaban singkat untuk pertanyaan yang paling sering diajukan pembaca tentang topik ini.

Bagaimana cara tahu route Next.js statis atau dynamic?

Jalankan next build lalu baca simbol di depan tiap route. Lingkaran berarti statis, titik penuh berarti HTML statis dari generateStaticParams, dan simbol ƒ berarti dirender saat diminta. Setelah itu pastikan dengan curl -I ke next start, karena next dev tidak pernah men-cache halaman.

Kenapa halaman Next.js saya masih menampilkan data lama setelah diubah?

Halaman yang diprerender menyimpan HTML saat build sampai ia direvalidate. Atur angka revalidate pada route, atau panggil revalidatePath atau revalidateTag setelah data berubah. Perlu diingat bahwa pada revalidate berbasis waktu, request pertama setelah kedaluwarsa masih menerima halaman lama sementara yang baru dibuat di latar belakang.

Apakah fetch di-cache secara default di Next.js 15?

Tidak. Next.js 15 tidak men-cache permintaan fetch secara default, dan Route Handler GET juga tidak di-cache. Aktifkan dengan cache force-cache atau next revalidate pada pemanggilan fetch, dan dynamic force-static pada route handler.

Kenapa revalidate tidak jalan saat saya pakai npm run dev?

Saat development, menurut dokumentasi halaman selalu dirender saat diminta dan tidak pernah di-cache, jadi jendela revalidate tidak punya apa-apa untuk kedaluwarsa. Jalankan npm run build dan npm start untuk menguji incremental static regeneration seperti di produksi.

Apakah memakai cookies() membuat seluruh halaman Next.js jadi dynamic?

Pada setup Next.js 15 biasa, ya. Komponen yang memakai cookies, headers, connection, draftMode, prop searchParams, unstable_noStore, atau fetch dengan no-store bergantung pada request, sehingga route tidak bisa diprerender. Simpan kode itu di route yang memang membutuhkannya, dan cek simbolnya di output next build.