Next.js 16: Cache Invalidation Mastery - updateTag vs revalidateTag - Eps 2

Masalah yang Muncul Setelah Setup

Episode 1 sudah membahas setup cache components dengan cacheComponents: true, perbedaan 'use cache' dengan 'use cache: private', dan bagaimana compiler otomatis menghasilkan cache keys. Semua berjalan lancar—konten statis muncul instant, data spesifik user ter-cache dengan baik, PPR membuat halaman terasa cepat.

Tapi ada satu pertanyaan besar yang belum terjawab: bagaimana cara invalidate cache saat data berubah?

Developer Cuma Pakai Satu Cara

Kebanyakan developer langsung pakai revalidateTag() untuk semua kasus. Ada produk baru? revalidateTag('products'). User klik like? revalidateTag('post-123'). Shopping cart berubah? revalidateTag('cart'). Semuanya pake satu function yang sama.

Masalahnya bukan karena revalidateTag() jelek. Function ini powerful dan bekerja dengan baik. Tapi ada situasi dimana user butuh feedback yang instant—tombol like yang langsung berubah, cart yang update tanpa delay, notifikasi yang muncul seketika. Dalam kasus seperti ini, revalidateTag() kurang responsive karena sifatnya yang stale-while-revalidate.

Hidden Gem: updateTag()

Solusi sebenarnya adalah updateTag()—function yang jarang developer tau dan hampir tidak pernah digunakan. Perbedaannya signifikan: revalidateTag() melakukan revalidasi di background sementara user masih melihat data lama. updateTag() langsung memaksa cache untuk update sebelum response dikirim ke user.

Episode ini fokus pada lima konsep: cacheTag() untuk label cache entries, cacheLife() untuk automatic expiration, revalidateTag() untuk background refresh, updateTag() untuk immediate update, dan trade-off antara immediate versus stale-while-revalidate.

Label Cache dengan cacheTag()

Bayangkan punya 100 halaman produk yang di-cache. Satu produk berubah harganya. Tanpa sistem tagging, harus invalidate semua cache atau cuma bisa invalidate berdasarkan route. Ini tidak efisien sama sekali.

cacheTag() memberikan label pada cache entries supaya bisa di-invalidate secara granular. Seperti memberikan sticker pada box penyimpanan—nanti tinggal cari box dengan sticker tertentu tanpa perlu bongkar semua gudang.

'use cache'

import { cacheTag } from 'next/cache'

export async function ProductPage({ params }: { params: { id: string } }) {
  cacheTag('products', `product-${params.id}`, 'catalog')

  const product = await fetch(`/api/products/${params.id}`).then(r => r.json())
  const reviews = await fetch(`/api/products/${params.id}/reviews`).then(r => r.json())

  return (
    <div>
      <h1>{product.name}</h1>
      <p>${product.price}</p>
      <div className="reviews">
        {reviews.map(review => (
          <p key={review.id}>{review.text}</p>
        ))}
      </div>
    </div>
  )
}

Component ini punya tiga tag: 'products' untuk semua produk, 'product-${id}' untuk item spesifik, 'catalog' untuk keseluruhan catalog. Saat invalidate tag 'products', semua produk kena. Saat invalidate 'product-123', cuma satu produk yang kena.

Kenapa Tags Penting

Tanpa tags: 1 product update = 100+ cache entries invalidated = server render 100+ halaman.

Dengan tags: 1 product update = 2 tags invalidated = server render 2-5 halaman saja.

Resource saving-nya signifikan. Build time lebih cepat. Server load lebih rendah. Halaman lain yang tidak terkait tetap ter-cache dan tetap cepat.

Expiration Otomatis dengan cacheLife()

cacheLife() mengatur berapa lama cache valid sebelum otomatis expired. Next.js 16 menyediakan built-in profiles yang bisa langsung dipakai.

Profile yang tersedia: 'max' untuk maksimal cache, 'hours' untuk beberapa jam, 'days' untuk beberapa hari, 'weeks' untuk beberapa minggu.

'use cache'

import { cacheLife } from 'next/cache'

export async function BlogPost() {
  cacheLife('days')  // Cache untuk beberapa hari

  const posts = await fetch('/api/blog').then(r => r.json())
  return <div>{posts.map(p => <article key={p.id}>{p.title}</article>)}</div>
}

Custom time juga bisa kalau butuh kontrol lebih presisi:

cacheLife('default', { revalidate: 300 })  // 5 menit

Tidak semua konten punya frekuensi update yang sama. Blog post jarang berubah—cache untuk days. Product inventory berubah tiap beberapa jam—cache untuk hours. Dashboard dengan real-time data—cache untuk minutes.

Pilih durasi sesuai keseimbangan antara kesegaran data versus performa. Terlalu lama = data tertinggal. Terlalu pendek = cache tidak efektif.

Kombinasi cacheTag() dan cacheLife()

Strategi paling powerful adalah gabungan keduanya. cacheLife() untuk revalidasi otomatis berdasarkan jadwal. cacheTag() untuk invalidasi on-demand saat ada perubahan data.

'use cache'

import { cacheTag, cacheLife } from 'next/cache'

export async function ProductCatalog() {
  cacheLife('hours')          // Auto expire tiap beberapa jam
  cacheTag('products:all')    // Bisa di-invalidate manual

  const products = await fetch('/api/products').then(r => r.json())

  return (
    <div className="grid">
      {products.map(p => (
        <div key={p.id}>
          <h3>{p.name}</h3>
          <p>${p.price}</p>
        </div>
      ))}
    </div>
  )
}

Dengan pattern ini, cache otomatis refresh setiap beberapa jam. Tapi kalau ada produk baru yang di-publish, developer bisa langsung invalidate tag 'products:all' tanpa tunggu expiration otomatis.

Server action untuk invalidate:

'use server'

import { revalidateTag } from 'next/cache'

export async function publishProduct(productId: string) {
  await db.product.publish(productId)
  revalidateTag('products:all')
}

Kombinasi terbaik—automatic untuk safety net, manual untuk kontrol langsung.

Konsep Stale-While-Revalidate

revalidateTag() bekerja dengan pattern yang namanya stale-while-revalidate. User melakukan action, server langsung kasih response dari cache yang ada, kemudian di background server mulai refresh cache supaya request berikutnya dapat data yang terbaru.

Ini berbeda dengan invalidasi cache tradisional yang nunggu sampai data baru siap sebelum kasih response. Dengan revalidateTag(), user tidak perlu tunggu—mereka langsung dapat response.

Pattern ini prioritaskan kecepatan. User experience terasa instant karena tidak ada waktu tunggu. Trade-off-nya adalah user mungkin melihat data lama sebentar—maksimal beberapa detik—setelah perubahan terjadi.

Timeline: Apa yang Terjadi di Balik Layar

Mari breakdown timeline untuk memahami proses ini:

t=0 ms: User klik tombol "Publish Post" di CMS. Browser kirim request ke server.

t=100 ms: Database berhasil update. Post status berubah dari draft jadi published.

t=200 ms: Server panggil revalidateTag('blog:all'). Cache di-marking sebagai perlu refresh. Bukan render ulang, cuma kasih tanda.

t=300 ms: Response dikirim ke browser. User langsung dapat konfirmasi bahwa post berhasil di-publish. Total waktu cuma 300ms.

t=500 ms: Proses background mulai revalidasi. Server render ulang component yang punya tag 'blog:all'. User sudah bisa lanjut kerja atau navigate ke halaman lain.

t=1000 ms: Cache baru selesai dan siap. Request berikutnya ke halaman blog akan dapat data yang sudah include post baru.

Total waktu dari action sampai cache fresh adalah 1 detik. Tapi user cuma ngerasa 300ms karena mereka tidak perlu tunggu proses revalidasi selesai.

Real Example: Update Inventory E-commerce

Scenario umum di e-commerce adalah inventory update. Product stock berubah karena ada pembelian atau restock dari supplier.

'use server'

import { revalidateTag } from 'next/cache'

export async function updateProductInventory(
  productId: string,
  newStock: number
) {
  await db.product.update({
    where: { id: productId },
    data: { stock: newStock }
  })

  // Invalidate multiple tags
  revalidateTag(`product-${productId}`)
  revalidateTag('products:all')
  revalidateTag('catalog')

  return { success: true }
}

Tiga tags di-invalidate sekaligus untuk memastikan semua halaman yang menampilkan produk ini akan di-refresh. User yang sedang lihat halaman produk mungkin masih lihat stock lama untuk beberapa detik, tapi begitu mereka refresh, data sudah fresh.

Kapan Menggunakan revalidateTag()

Gunakan revalidateTag() untuk scenario ini:

Blog post published: Author klik publish, post muncul di blog list. Delay beberapa detik tidak masalah.

Product catalog updated: Admin update deskripsi produk atau gambar. Perubahan tidak time-sensitive.

CMS content changes: Marketing team update homepage banner atau landing page content. Perubahan ini bersifat editorial dan tidak mempengaruhi functionality.

Newsletter published: Send email blast ke subscribers dengan link ke artikel baru. Tidak masalah kalau artikel muncul di website beberapa detik setelah email terkirim.

Category restructure: E-commerce merubah struktur kategori produk. Ini perubahan besar yang mempengaruhi banyak pages. Revalidasi di background lebih aman karena tidak overwhelm server.

Prinsip dasarnya: gunakan revalidateTag() kalau user expectation adalah "perubahan akan terlihat sebentar lagi" bukan "perubahan harus terlihat sekarang juga".

Trade-off: Fast Response vs Always Fresh

revalidateTag() memilih fast response dengan resiko data sedikit tertinggal untuk sesaat. Alternative-nya adalah selalu fresh tapi dengan response yang lebih lambat karena harus nunggu render selesai.

Comparison konkret:

Fast response (revalidateTag):

  • Response time: 200-500ms
  • Data freshness: Data lama muncul 1-2 detik pertama
  • User experience: Terasa instant
  • Server load: Terdistribusi sepanjang waktu
  • Cocok untuk: Update yang tidak critical

Always fresh (tunggu render):

  • Response time: 1-3 detik
  • Data freshness: Selalu fresh
  • User experience: Ada delay yang terasa
  • Server load: Lonjakan saat update
  • Cocok untuk: Data yang critical

revalidateTag() cocok untuk mayoritas use cases karena data yang sedikit tertinggal tidak mengganggu pengalaman user. User lebih suka dapat feedback instant daripada nunggu untuk data yang 100% fresh.

Ada exception dimana data yang selalu fresh lebih penting: financial transactions, inventory critical untuk checkout process, user authentication state. Untuk kasus seperti ini, episode berikutnya akan bahas updateTag() yang memberikan update langsung tanpa jeda waktu.

Pattern ini sudah terbukti di aplikasi production dengan traffic tinggi. Instagram, Twitter, Facebook semua pakai variant dari stale-while-revalidate untuk feed updates. User tidak mengeluh karena mereka prioritaskan speed.

Perbedaan yang Signifikan

updateTag() adalah function yang jarang developer tau tapi punya dampak besar ke pengalaman user. Perbedaan utamanya dengan revalidateTag() adalah timing:

updateTag() melakukan update cache langsung sebelum response dikirim. User tidak pernah melihat data lama. Response sedikit lebih lambat, tapi data dijamin fresh.

revalidateTag() langsung kirim response dari cache yang ada, lalu refresh di background. Response terasa instant, tapi user mungkin melihat data lama sebentar.

Gunakan updateTag() kalau user berharap "perubahan harus terlihat sekarang juga". Gunakan revalidateTag() kalau "perubahan akan terlihat sebentar lagi" sudah cukup.

Timeline Comparison

Mari bandingkan timeline keduanya untuk scenario yang sama: user klik tombol like di post.

Timeline updateTag():

t=0: User klik like t=100: Database update t=200: updateTag() - cache di-refresh t=800: Response dikirim dengan data fresh

Total: 800ms, user langsung lihat hasil baru.

Timeline revalidateTag():

t=0: User klik like t=100: Database update t=200: revalidateTag() - cache di-marking t=300: Response dikirim dengan data lama t=500: Background render selesai t=1000+: User refresh, baru lihat data baru

Total: 300ms response, tapi data fresh butuh waktu lebih lama.

Perbedaan utama: updateTag() = data fresh langsung, response agak lambat. revalidateTag() = response cepat, data fresh nanti.

Real Examples yang Tepat untuk updateTag()

Gunakan updateTag() untuk user actions yang butuh feedback langsung:

Like/unlike post: User expect icon berubah instantly. Kalau tetap sama, user bingung apakah action berhasil.

'use server'

import { updateTag } from 'next/cache'

export async function toggleLike(postId: string) {
  await db.post.toggleLike(postId)
  updateTag(`post-${postId}`)
}

Follow/unfollow user: Button harus langsung berubah jadi "Following" untuk user confidence.

Toggle notification: User toggle on/off, status harus langsung berubah. Kalau tidak, mereka akan klik lagi dan toggle balik.

Form submission: User expect lihat data baru langsung setelah submit, bukan data lama.

Add to cart: Cart count harus langsung bertambah. Critical untuk e-commerce UX.

Prinsip dasarnya: kalau user melakukan action dan expect visual feedback immediate, pakai updateTag().

Decision Matrix: Kapan Pakai Apa

Like post → updateTag() Reason: User perlu konfirmasi visual instant

Publish blog → revalidateTag() Reason: Delay 1-2 detik acceptable

Toggle preference → updateTag() Reason: User akan bingung kalau status tidak berubah

Update inventory → revalidateTag() Reason: Background process, tidak critical

Add to cart → updateTag() Reason: Critical untuk shopping experience

Publish newsletter → revalidateTag() Reason: Background task, tidak blocking

Pattern: user-triggered actions yang butuh visual feedback immediate pakai updateTag(). Background updates atau editorial changes pakai revalidateTag().

Implementation Pattern

Pattern untuk updateTag() sangat straightforward:

'use server'

import { updateTag } from 'next/cache'

export async function likePost(postId: string) {
  await db.post.addLike(postId)
  updateTag(`post-${postId}`)
}

Syntax identik dengan revalidateTag(). Perbedaannya cuma di behavior: updateTag() nunggu cache refresh sebelum return, revalidateTag() langsung return.

Pattern untuk form submission:

'use server'

import { updateTag } from 'next/cache'
import { redirect } from 'next/navigation'

export async function updateProfile(formData: FormData) {
  const userId = await getCurrentUserId()

  await db.user.update({
    where: { id: userId },
    data: {
      name: formData.get('name'),
      bio: formData.get('bio')
    }
  })

  updateTag(`user-${userId}`)
  redirect('/profile')
}

User submit form, data di-update, cache di-refresh, baru redirect. User langsung lihat data baru tanpa refresh manual.

Kenapa Ini Hidden Gem

updateTag() adalah hidden gem karena beberapa alasan:

Sedikit developer yang tau: Dokumentasi Next.js lebih fokus ke revalidateTag(). updateTag() jarang disebutkan di tutorial atau blog posts.

Dampak UX yang besar: Perbedaan antara data instant versus data yang muncul 1-2 detik kemudian sangat terasa. User modern mengharapkan feedback instant.

Mudah diimplementasi: Cuma ganti revalidateTag() jadi updateTag(). Tidak ada kompleksitas tambahan.

Powerful untuk forms: Form submissions jadi jauh lebih smooth. Data baru langsung terlihat setelah submit.

Trade-off-nya adalah response time sedikit lebih lambat. Tapi untuk user-triggered actions, extra 500ms itu bisa diterima karena mereka dapat feedback yang langsung dan akurat.

Kebanyakan apps seharusnya pakai kombinasi: updateTag() untuk user actions, revalidateTag() untuk background updates. Tapi karena kurangnya awareness, mayoritas developer cuma pakai revalidateTag() dan melewatkan kesempatan untuk meningkatkan UX secara signifikan.

Scenario-Based Decision Tree

Mari breakdown berbagai scenario umum dan pilih strategi yang tepat:

Blog post published Keputusan: revalidateTag() Alasan: Author bisa tunggu beberapa detik. Tidak mengganggu workflow mereka.

User submits form Keputusan: updateTag() Alasan: User mengharapkan lihat hasil form submission langsung. Critical untuk kepercayaan user.

Product inventory updated Keputusan: revalidateTag() Alasan: Inventory sync dari sistem eksternal. Update di background sudah cukup.

User toggles dark mode Keputusan: updateTag() Alasan: Visual feedback harus instant. User akan klik berkali-kali kalau tidak langsung berubah.

CMS content changed Keputusan: revalidateTag() Alasan: Perubahan editorial. Content team bisa refresh manual kalau perlu lihat langsung.

Shopping cart modified Keputusan: updateTag() Alasan: Cart count harus langsung update. Critical untuk shopping experience dan mencegah kebingungan.

Pattern yang muncul: kalau user melakukan action langsung dan mengharapkan hasil visual, pakai updateTag(). Kalau perubahan dari sistem atau proses background, pakai revalidateTag().

Rule of Thumb

Tiga pertanyaan untuk menentukan strategi:

User mengharapkan feedback langsung? Ya → updateTag() Contoh: Like button, follow button, toggle settings, add to cart

Delay sedikit tidak masalah? Ya → revalidateTag() Contoh: Publish content, update inventory, sync data eksternal

Konten yang critical? Ya → Kombinasi updateTag() + cacheLife() Contoh: Data finansial, autentikasi user, status pembayaran

Rule ini berlaku untuk 90% use cases. Sisanya butuh penilaian berdasarkan konteks spesifik aplikasi.

Practical Patterns

Ada tiga pattern utama yang bisa dipakai:

Pattern 1: Immediate Feedback

'use server'

import { updateTag } from 'next/cache'

export async function toggleFeature(featureId: string) {
  await db.feature.toggle(featureId)
  updateTag(`feature-${featureId}`)
}

Gunakan kalau: User action yang butuh konfirmasi visual instant. Form submissions, toggles, preferensi user.

Pattern 2: Background Revalidation

'use server'

import { revalidateTag } from 'next/cache'

export async function syncInventory(productId: string) {
  await externalAPI.updateInventory(productId)
  revalidateTag(`product-${productId}`)
}

Gunakan kalau: Update dari sistem eksternal, scheduled tasks, perubahan editorial.

Pattern 3: Hybrid (Time + On-Demand)

'use cache'

import { cacheTag, cacheLife } from 'next/cache'

export async function ProductPricing() {
  cacheLife('hours')              // Auto-refresh tiap jam
  cacheTag('pricing:all')         // Bisa di-invalidate manual

  const prices = await fetch('/api/prices').then(r => r.json())
  return <div>{/* render prices */}</div>
}

Server action:

'use server'

import { updateTag } from 'next/cache'

export async function updatePricing() {
  await db.pricing.update()
  updateTag('pricing:all')  // Paksa update langsung
}

Gunakan kalau: Data yang critical dan perlu keseimbangan antara automatic refresh dengan manual control. Data finansial, pricing, autentikasi.

Pattern 3 adalah rekomendasi untuk aplikasi production karena kombinasi safety net (automatic) dengan flexibility (manual).

Performance Considerations

Ada beberapa faktor yang perlu dipertimbangkan:

Frequency of Invalidations

High frequency (>10x per menit): Pertimbangkan apakah cache perlu sama sekali. Kalau invalidate terlalu sering, overhead-nya lebih besar dari benefit.

Medium frequency (1-10x per menit): revalidateTag() lebih optimal. Load terdistribusi, tidak membebani server.

Low frequency (<1x per menit): updateTag() atau revalidateTag() keduanya OK. Pilih berdasarkan ekspektasi user, bukan performa.

Number of Affected Tags

Single tag: updateTag() masih oke. Render time minimal.

Multiple tags (2-5): updateTag() masih OK kalau user mengharapkan feedback langsung. Pertimbangkan revalidateTag() kalau tidak critical.

Many tags (>5): revalidateTag() lebih aman. Hindari blocking response terlalu lama.

Cache Hit Rates

Monitor cache hit rates untuk validasi strategi. Target: >80% hit rate.

Low hit rate (<50%): Cache invalidation terlalu agresif atau durasi cache terlalu pendek. Pertimbangkan tingkatkan cacheLife() atau kurangi frekuensi invalidation.

High hit rate (>90%): Cache mungkin terlalu lama. Validasi apakah user melihat data yang ketinggalan. Kalau ya, tingkatkan frekuensi invalidation atau kurangi cacheLife().

Tools untuk monitoring: Vercel Analytics, custom logging, atau Next.js built-in cache metrics.

Jangan optimasi terlalu dini. Mulai dengan strategi yang sederhana (mostly revalidateTag() dengan cacheLife()), monitor performa, sesuaikan berdasarkan data aktual.

Tiga Strategi Cache Invalidation

Ada tiga pendekatan untuk mengatur cache invalidation. Masing-masing punya keseimbangan yang berbeda.

Strategi 1: Time-based Only

Strategi ini cuma pakai cacheLife() tanpa cacheTag(). Cache otomatis expired setelah durasi tertentu.

'use cache'

import { cacheLife } from 'next/cache'

export async function ProductList() {
  cacheLife('hours')

  const products = await fetch('/api/products').then(r => r.json())
  return <div>{products.map(p => <div key={p.id}>{p.name}</div>)}</div>
}

Pro: Setup sangat simple. Tidak perlu mikir tentang invalidation manual. Cocok untuk konten yang jadwal update-nya bisa diprediksi.

Con: Data bisa tertinggal sampai cache expired. Kalau ada produk baru yang urgent, harus tunggu sampai cache expired secara otomatis. Tidak ada kontrol manual.

Strategi 2: On-demand Only

Strategi ini cuma pakai cacheTag() tanpa cacheLife(). Cache bertahan selamanya sampai di-invalidate manual.

'use cache'

import { cacheTag } from 'next/cache'

export async function ProductList() {
  cacheTag('products:all')

  const products = await fetch('/api/products').then(r => r.json())
  return <div>{products.map(p => <div key={p.id}>{p.name}</div>)}</div>
}

Server action:

'use server'

import { revalidateTag } from 'next/cache'

export async function addProduct(data: FormData) {
  await db.product.create({ data })
  revalidateTag('products:all')
}

Pro: Data selalu fresh karena setiap perubahan pasti di-invalidate. Kontrol penuh kapan cache di-refresh.

Con: Gampang lupa invalidate. Kalau ada satu tempat yang update data tapi lupa panggil revalidateTag(), cache jadi tertinggal permanent. Butuh disiplin tim.

Strategi 3: Hybrid - Recommended

Strategi ini kombinasi cacheLife() dan cacheTag(). Cache expired otomatis berdasarkan waktu, tapi juga bisa di-invalidate manual kalau perlu.

'use cache'

import { cacheTag, cacheLife } from 'next/cache'

export async function BlogPosts() {
  cacheLife('days')       // Auto revalidate per hari
  cacheTag('blog:all')    // Juga bisa on-demand

  return await db.post.findMany()
}

Server action:

'use server'

import { revalidateTag } from 'next/cache'

export async function publishPost(postId: string) {
  await db.post.publish(postId)
  revalidateTag('blog:all')  // Invalidate langsung
}

Pro: Kombinasi terbaik. Ada jaring pengaman dari expiration otomatis. Ada fleksibilitas dari invalidation manual. Kalau lupa invalidate, cache tetap fresh dalam 24 jam.

Con: Setup sedikit lebih kompleks. Butuh pikir tentang durasi cache yang tepat. Tapi kompleksitas ini sebanding dengan keandalan yang didapat.

Rekomendasi: Pakai strategi 3 untuk aplikasi production. Strategi 1 oke untuk konten yang jarang berubah seperti dokumentasi. Strategi 2 berisiko untuk tim besar karena mudah lupa invalidate.

Example Implementation Lengkap

Berikut contoh implementasi lengkap untuk blog system:

// Component dengan hybrid strategy
'use cache'

import { cacheTag, cacheLife } from 'next/cache'

export async function BlogPosts() {
  cacheLife('days')       // Auto expire setiap hari
  cacheTag('blog:all')    // Manual invalidation tersedia

  const posts = await db.post.findMany({
    where: { published: true },
    orderBy: { publishedAt: 'desc' }
  })

  return (
    <div className="posts">
      {posts.map(post => (
        <article key={post.id}>
          <h2>{post.title}</h2>
          <p>{post.excerpt}</p>
        </article>
      ))}
    </div>
  )
}

Server action untuk publish post:

'use server'

import { revalidateTag } from 'next/cache'

export async function publishPost(postId: string) {
  // Update database
  await db.post.update({
    where: { id: postId },
    data: {
      published: true,
      publishedAt: new Date()
    }
  })

  // Invalidate cache langsung
  revalidateTag('blog:all')
}

Server action untuk unpublish:

'use server'

import { revalidateTag } from 'next/cache'

export async function unpublishPost(postId: string) {
  await db.post.update({
    where: { id: postId },
    data: { published: false }
  })

  revalidateTag('blog:all')
}

Dengan setup ini, cache otomatis refresh setiap hari. Tapi kalau admin publish atau unpublish post, cache langsung di-invalidate. User tidak perlu tunggu sampai besok untuk lihat perubahan.

Kalau ada bug di code dan lupa panggil revalidateTag(), skenario terburuk adalah user lihat data yang tertinggal selama 24 jam. Bukan tertinggal permanen seperti strategi 2.

Setup Data Queries

Mari lihat bagaimana setup cache untuk aplikasi e-commerce yang lengkap.

Product Detail dengan Hybrid Strategy

'use cache'

import { cacheTag, cacheLife } from 'next/cache'

export async function getProduct(id: string) {
  cacheLife('hours')                    // Auto expire tiap jam
  cacheTag('products:all', `product-${id}`)  // Manual invalidation

  return await db.product.findUnique({
    where: { id },
    include: {
      images: true,
      reviews: true
    }
  })

  // Demo data
  // {
  //   id: "prod_001",
  //   name: "Nike Air Max 2024",
  //   price: 1899000,
  //   stock: 45,
  //   category: "shoes",
  //   images: [
  //     { url: "/images/nike-air-max-1.jpg" },
  //     { url: "/images/nike-air-max-2.jpg" }
  //   ],
  //   reviews: [
  //     { id: "rev_001", rating: 5, text: "Keren banget!" }
  //   ]
  // }
}

Product detail perlu fresh tapi tidak perlu real-time. Cache untuk hours dengan opsi invalidation manual kalau ada update urgent.

Product List dengan Similar Strategy

'use cache'

import { cacheTag, cacheLife } from 'next/cache'

export async function getProductList(category?: string) {
  cacheLife('hours')
  cacheTag('products:all', category ? `category-${category}` : 'all-products')

  return await db.product.findMany({
    where: category ? { category } : {},
    orderBy: { createdAt: 'desc' }
  })

  // Demo data
  // [
  //   {
  //     id: "prod_001",
  //     name: "Nike Air Max 2024",
  //     price: 1899000,
  //     stock: 45,
  //     category: "shoes"
  //   },
  //   {
  //     id: "prod_002",
  //     name: "Adidas Ultraboost 22",
  //     price: 2299000,
  //     stock: 32,
  //     category: "shoes"
  //   },
  //   {
  //     id: "prod_003",
  //     name: "Puma RS-X",
  //     price: 1499000,
  //     stock: 18,
  //     category: "shoes"
  //   }
  // ]
}

Product list juga cache untuk hours. Tapi pakai tag berbeda per kategori supaya bisa invalidate granular.

Shopping Cart dengan Private Cache

'use cache: private'

import { cacheTag, cacheLife } from 'next/cache'
import { cookies } from 'next/headers'

export async function getShoppingCart(userId: string) {
  cacheLife('default', { revalidate: 30 })  // 30 detik
  cacheTag(`cart-${userId}`)

  return await db.cart.findUnique({
    where: { userId },
    include: { items: true }
  })

  // Demo data
  // {
  //   id: "cart_user_123",
  //   userId: "user_123",
  //   items: [
  //     {
  //       id: "item_001",
  //       productId: "prod_001",
  //       productName: "Nike Air Max 2024",
  //       quantity: 2,
  //       price: 1899000
  //     },
  //     {
  //       id: "item_002",
  //       productId: "prod_003",
  //       productName: "Puma RS-X",
  //       quantity: 1,
  //       price: 1499000
  //     }
  //   ],
  //   total: 5297000
  // }
}

Shopping cart pakai private cache karena data spesifik user. Cache cuma 30 detik karena cart sering berubah.

Server Actions untuk Cart Operations

Add to Cart - Immediate Update

'use server'

import { updateTag } from 'next/cache'

export async function addToCart(userId: string, productId: string) {
  await db.cartItem.create({
    data: {
      userId,
      productId,
      quantity: 1
    }
  })

  updateTag(`cart-${userId}`)  // User langsung lihat perubahan

  // Demo action:
  // Input: userId="user_123", productId="prod_001"
  // Result: Cart item ditambahkan, cache di-refresh instantly
  // User lihat cart count: 2 → 3
}

Add to cart pakai updateTag() karena user mengharapkan cart count langsung bertambah. Critical untuk pengalaman user.

Remove from Cart - Immediate Update

'use server'

import { updateTag } from 'next/cache'

export async function removeFromCart(cartItemId: string, userId: string) {
  await db.cartItem.delete({
    where: { id: cartItemId }
  })

  updateTag(`cart-${userId}`)  // User langsung lihat item hilang
}

Remove juga pakai updateTag() untuk feedback instant.

Update Inventory - Background Update

'use server'

import { revalidateTag } from 'next/cache'

export async function updateProductInventory(productId: string, stock: number) {
  await db.product.update({
    where: { id: productId },
    data: { stock }
  })

  revalidateTag(`product-${productId}`)
  revalidateTag('products:all')

  // Demo action:
  // Input: productId="prod_001", stock=45 → 38
  // Result: Inventory di-update di background
  // Admin response: 300ms
  // User lihat update: 1-2 detik kemudian
}

Inventory update dari admin atau sistem eksternal. Revalidation di background sudah cukup karena tidak critical untuk feedback langsung.

Publish Product - Kombinasi Both

'use server'

import { updateTag, revalidateTag } from 'next/cache'

export async function publishProduct(productId: string) {
  await db.product.update({
    where: { id: productId },
    data: {
      published: true,
      publishedAt: new Date()
    }
  })

  updateTag(`product-${productId}`)      // Detail page langsung
  revalidateTag('products:all')           // List page background

  // Demo action:
  // Input: productId="prod_004"
  // Result:
  // - Product detail page: Update instantly
  // - Product list page: Update dalam 1-2 detik
  // - Best of both: Critical page cepat, non-critical background
}

Publish product pakai kombinasi. Detail page butuh update langsung, tapi list page bisa di-update di background.

Full Implementation

Component Level Setup

// Product Detail Page
export default async function ProductPage({ params }: { params: { id: string } }) {
  const product = await getProduct(params.id)

  return (
    <div>
      <h1>{product.name}</h1>
      <p>${product.price}</p>
      <AddToCartButton productId={product.id} />
    </div>
  )
}

// Product List Page
export default async function CategoryPage({ params }: { params: { category: string } }) {
  const products = await getProductList(params.category)

  return (
    <div className="grid">
      {products.map(p => (
        <ProductCard key={p.id} product={p} />
      ))}
    </div>
  )
}

// Shopping Cart Page
export default async function CartPage() {
  const userId = await getCurrentUserId()
  const cart = await getShoppingCart(userId)

  return (
    <div>
      <h1>Your Cart ({cart.items.length})</h1>
      {cart.items.map(item => (
        <CartItem key={item.id} item={item} userId={userId} />
      ))}
    </div>
  )
}

Action Level Invalidation

Client components yang trigger actions:

'use client'

export function AddToCartButton({ productId }: { productId: string }) {
  const [pending, setPending] = useState(false)

  async function handleAdd() {
    setPending(true)
    await addToCart(userId, productId)
    setPending(false)
  }

  return (
    <button onClick={handleAdd} disabled={pending}>
      {pending ? 'Adding...' : 'Add to Cart'}
    </button>
  )
}

Combined Strategy Benefits

Dengan setup ini, aplikasi punya performa optimal:

Product pages ter-cache untuk hours. Server tidak terbebani dengan banyak requests. Tapi kalau admin publish produk baru atau update inventory, cache langsung di-refresh.

Cart operations pakai updateTag() jadi pengalaman user smooth. User klik add to cart, langsung lihat cart count bertambah. Tidak ada kebingungan atau double-click.

Inventory updates pakai revalidateTag() jadi tidak menghambat. Admin update 100 produk sekaligus tidak bikin server lemot karena revalidation terjadi di background.

Metrics dan Benefits

Cart Updates Instant

Response time: 200-500ms untuk add/remove cart. User langsung lihat perubahan. Tingkat konversi meningkat karena pengalaman yang smooth.

Inventory Updates Soon

Response time: 300ms untuk admin. Inventory update terlihat dalam 1-2 detik. Bisa diterima untuk workflow admin. Server tidak terbebani dengan render bersamaan.

Optimal Balance

Cache hit rate: >80% untuk product pages. Server load berkurang 70% dibanding tanpa cache. Pengalaman user tetap fresh karena strategi hybrid.

Setup ini bisa di-scale untuk e-commerce dengan ribuan produk dan ribuan user concurrent. Kombinasi updateTag() untuk user-facing actions dan revalidateTag() untuk background updates adalah titik optimal untuk performa dan pengalaman user.

Yang Harus Diingat

Episode ini membahas cache invalidation secara mendalam. Ada enam poin kunci yang perlu dipahami:

1. cacheTag() untuk kontrol granular

Tag memberikan label pada cache entries supaya bisa di-invalidate secara spesifik. Tidak perlu invalidate seluruh website, cukup tag yang relevan saja. Efisiensi maksimal.

2. cacheLife() untuk expiration otomatis

Built-in profiles seperti hours, days, weeks bikin setup simple. Cache otomatis expired berdasarkan waktu. Safety net kalau lupa invalidate manual.

3. updateTag() untuk update langsung - hidden gem!

Ini adalah function yang jarang developer tau tapi punya dampak besar. Pakai untuk user actions yang butuh feedback instant: like button, add to cart, toggle settings. User langsung lihat perubahan tanpa delay.

4. revalidateTag() untuk background refresh

Pakai untuk perubahan yang tidak perlu instant: publish blog, update inventory, sync data eksternal. Response cepat, revalidation terjadi di background.

5. Hybrid approach adalah praktik terbaik

Kombinasi cacheLife() + cacheTag() + updateTag()/revalidateTag() adalah strategi terbaik. Ada expiration otomatis sebagai jaring pengaman. Ada invalidation manual untuk kontrol. Andal dan fleksibel.

6. Pilih berdasarkan ekspektasi user

User mengharapkan perubahan langsung? Pakai updateTag(). User bisa tunggu sebentar? Pakai revalidateTag(). Ini bukan tentang teknis, tapi tentang pengalaman user.

Episode Berikutnya

Episode 3 akan fokus pada Enhanced Routing di Next.js 16. Dua fitur besar yang akan dibahas:

Layout Deduplication: Next.js 16 otomatis detect shared layouts dan tidak render ulang. Navigation jadi lebih cepat karena cuma render yang berubah saja.

Incremental Prefetching: Next.js cerdas dalam prefetch links. Tidak download semua sekaligus, tapi bertahap sesuai prioritas. Hemat bandwidth dan pengalaman user smooth.

Episode 3 akan deep dive ke optimasi routing yang sudah di-solve otomatis oleh Next.js 16. Developer tidak perlu mikir lagi tentang performa routing.

Belajar Lebih Lanjut

Untuk dokumentasi resmi Next.js tentang caching, kunjungi nextjs.org/docs/app/api-reference. Dokumentasi ini lengkap dengan referensi API dan praktik terbaik.

Untuk pembelajaran lebih mendalam, BuildWithAngga menyediakan berbagai course Next.js yang cover fundamental hingga advanced topics. Ikuti update course terbaru di BuildWithAngga untuk materi caching dan optimasi performa.

Praktik langsung adalah cara terbaik untuk menguasai cache invalidation. Mulai dengan strategi sederhana, monitor hasilnya, sesuaikan berdasarkan data. Jangan optimasi terlalu dini. Build, measure, iterate.

Cache invalidation adalah skill yang powerful kalau dikuasai dengan benar. Perbedaan antara aplikasi yang terasa lambat versus aplikasi yang terasa instant sering cuma masalah strategi caching yang tepat.