kodingup.camp

Beranda/Next.js Mastery: Dasar, Routing & Rendering

Caching & Revalidasi

Modul 8 dari 107 menit bacaCaching and Revalidation

Isi modul

    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:

    Hasil gabungan inilah Partial Prerendering. Kerangka statis bisa disajikan langsung dari CDN.

    "use cache" di tiga level

    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:

    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:

    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)

    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

    1. Memakai export const revalidate atau dynamic = 'force-dynamic' di proyek Cache Components. Build menolak. Pakai "use cache" + cacheLife, atau connection() + <Suspense> untuk data per request.
    2. revalidateTag('posts') dengan satu argumen. Deprecated di Next.js 16. Tulis revalidateTag('posts', 'max'), atau updateTag('posts') di Server Action.
    3. 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.
    4. Memanggil updateTag di Route Handler. updateTag khusus Server Action. Di Route Handler pakai revalidateTag.
    5. 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

    1. Mudah, Buat fungsi getKurs() yang mengambil https://open.er-api.com/v6/latest/USD, di-cache dengan profil hours dan tag kurs. Tampilkan kurs IDR di halaman /kurs.
    2. Sedang, Tambahkan tombol "Perbarui kurs" di halaman /kurs berupa form yang memanggil Server Action berisi updateTag('kurs').
    3. Menantang, Buat halaman /menu yang menampilkan daftar menu (di-cache days, tag menu), sapaan berdasarkan cookie nama (di-stream), dan jam server (per request). Pastikan npm run build lolos 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

    Video pendamping

    Kalau penjelasan di atas belum masuk, tonton ini. Video dari kanal YouTube pihak lain.

    Next.js Finally Fixed CachingWeb Dev Simplified · Bahasa InggrisBuka di YouTube

    Kuis pemahaman

    Pilih satu jawaban per soal. Penjelasan muncul setelah kamu menjawab.