Giriş
Bu yazı serimizin beşinci bölümüne hoş geldiniz! Şimdiye kadar React Server Components ile veri çekme stratejilerini, Server Actions ile güvenli form işlemlerini, Middleware katmanında Edge Runtime kalıplarını ve Paralel Rotalar ile Intercepting Routes konularını derinlemesine ele aldık. Şimdi sıra, Next.js App Router'ın belki de en karmaşık — ama bir o kadar da kritik — konusuna geldi: önbellekleme (caching) ve revalidation.
Eğer bir Next.js uygulamasını üretime taşıdıysanız ve "verilerim neden güncellenmiyor?" diye saçlarınızı yoldaysanız, inanın yalnız değilsiniz. Hepimiz oradaydık.
Next.js'in çok katmanlı önbellek mimarisi, doğru anlaşıldığında inanılmaz performans sağlıyor. Yanlış anlaşıldığında ise bayat veri kabusu. Ortası yok gibi bir şey açıkçası.
Bu rehberde Next.js 16'nın getirdiği use cache direktifi, cacheTag, cacheLife, revalidateTag, updateTag ve ISR (Incremental Static Regeneration) stratejilerini gerçek üretim ortamı senaryolarıyla birlikte ele alacağız. Hazırsanız başlayalım.
Next.js'in Çok Katmanlı Önbellek Mimarisi
Next.js App Router tek bir önbelleğe sahip değil — birbiriyle etkileşen altı farklı önbellek katmanı barındırıyor. Çoğu üretim ortamı sorununun kökünde bu katmanların karıştırılması yatıyor. Gelin bunları tek tek inceleyelim.
1. Request Memoization (İstek Çoğaltma Önleme)
Aynı render döngüsü içinde tekrarlanan fetch çağrılarını otomatik olarak birleştiren katman. Farklı bileşenler aynı veriyi istediğinde tek bir istek yapılıyor — düşünsenize, bunu elle yapmaya kalkışsanız ne kadar uğraştırıcı olurdu.
// Bu iki bileşen aynı render döngüsünde çalıştığında
// sadece tek bir fetch isteği yapılır
async function Header() {
const user = await fetch('/api/user')
return <nav>{user.name}</nav>
}
async function Sidebar() {
const user = await fetch('/api/user')
return <aside>{user.email}</aside>
}
2. Data Cache (Veri Önbelleği)
Fetch sonuçlarını istekler arasında saklayan katman. Önemli bir not: Next.js 15 ve sonrasında fetch istekleri varsayılan olarak önbelleğe alınmıyor. Bu, eski sürümlerdeki o sinir bozucu sürpriz önbellek davranışlarını ortadan kaldırdı (sonunda!).
// Açık önbellek ayarı
const data = await fetch('https://api.example.com/posts', {
cache: 'force-cache' // Açıkça önbelleğe al
})
// Hiç önbelleğe alma
const live = await fetch('https://api.example.com/stock', {
cache: 'no-store' // Her zaman taze veri
})
3. Full Route Cache (Tam Rota Önbelleği)
Statik rotaların tamamını build zamanında render edip CDN üzerinden sunan katman. Dinamik API'ler (cookies(), headers(), searchParams) kullanmayan sayfalar otomatik olarak bu kategoriye giriyor.
4. Router Cache (İstemci Tarafı Önbellek)
Bu katman tarayıcıda çalışıyor ve sayfalar arası gezinmeyi hızlandırıyor. Dinamik sayfalar 30 saniye, statik sayfalar ise 5 dakika boyunca önbelleğe alınıyor. Ve burada dikkat edilmesi gereken bir şey var: bu süre, yapılandırmadan bağımsız olarak minimum 30 saniye uygulanıyor. Yani bu sürenin altına inemiyorsunuz.
5. Time-Based Revalidation (Zaman Tabanlı Yeniden Doğrulama / ISR)
Önbelleğe alınan veriyi belirli bir süre sonra otomatik olarak tazeleyen mekanizma. Detaylarına birazdan geleceğiz.
6. On-Demand Revalidation (İsteğe Bağlı Yeniden Doğrulama)
Bir olay tetiklendiğinde önbelleği programatik olarak geçersiz kılma yöntemi. revalidateTag, updateTag ve revalidatePath bu katmanın araçları.
ISR (Incremental Static Regeneration) Stratejileri
ISR, bence Next.js'in en güçlü önbellekleme stratejisi. Stale-While-Revalidate (SWR) modelini kullanıyor: kullanıcıya önbellekteki mevcut sayfa anında sunulurken arka planda yeni versiyon oluşturuluyor. Yani kullanıcı beklemek zorunda kalmıyor — herkes mutlu.
Zaman Tabanlı ISR Akışı
// app/blog/[slug]/page.tsx
export const revalidate = 3600 // 1 saat
export default async function BlogPost({ params }) {
const { slug } = await params
const post = await fetch(`https://api.example.com/posts/${slug}`, {
next: { revalidate: 3600 }
})
return (
<article>
<h1>{post.title}</h1>
<div>{post.content}</div>
</article>
)
}
Bu yapıda akış şu şekilde işliyor:
- İlk istek geldiğinde sayfa render edilir ve önbelleğe alınır.
- Sonraki 3600 saniye boyunca tüm istekler önbellekten sunulur.
- 3600 saniye geçtikten sonra gelen ilk istek hâlâ eski (stale) veriyi görür, ama arka planda yeni versiyon oluşturulur.
- Yeniden oluşturma başarılı olduğunda önbellek güncellenir ve sonraki istekler taze veriyi alır.
Farklı Veri Türleri İçin ISR Stratejisi Matrisi
Her veri türü için aynı stratejiyi kullanmak yaygın bir hata. Veri değişim sıklığına göre farklı süre değerleri belirlemek çok daha mantıklı:
// Blog yazıları — saatlik revalidation yeterli
const post = await fetch(url, {
next: { revalidate: 3600 }
})
// Ürün fiyatları — 5 dakikada bir güncelle
const price = await fetch(priceUrl, {
next: { revalidate: 300 }
})
// Kullanıcıya özel veri — hiç önbelleğe alma
const profile = await fetch(profileUrl, {
cache: 'no-store'
})
// Neredeyse hiç değişmeyen ayarlar — günlük
const settings = await fetch(settingsUrl, {
next: { revalidate: 86400 }
})
Dikkat: Bir rota içinde birden fazla fetch isteği varsa ve her birinin farklı revalidate süresi varsa, ISR için en düşük süre geçerli oluyor. Yani sayfada hem 1 saatlik hem 24 saatlik bir fetch varsa, sayfa 1 saatte bir yeniden oluşturulacak. Ancak Data Cache katmanında her isteğin kendi süresini bağımsız olarak uyguladığını unutmayın.
Next.js 16: use cache Direktifi ve Cache Components
Next.js 16 ile birlikte önbellekleme paradigması kökten değişti. Artık önbellekleme tamamen açık (opt-in) bir süreç — hiçbir şey varsayılan olarak önbelleğe alınmıyor. Dürüst olmak gerekirse, bu değişikliği uzun süredir bekliyordum. Eski sürümlerdeki sürpriz önbellek davranışları gerçekten baş ağrıtıcıydı.
use cache Direktifini Etkinleştirme
// next.config.ts
import type { NextConfig } from 'next'
const nextConfig: NextConfig = {
cacheComponents: true,
}
export default nextConfig
Not: Eski experimental.dynamicIO bayrağı, Next.js 16'da cacheComponents olarak yeniden adlandırıldı.
Sayfa Düzeyinde Önbellekleme
// app/products/page.tsx
import { cacheLife, cacheTag } from 'next/cache'
export default async function ProductsPage() {
'use cache'
cacheLife('hours')
cacheTag('products')
const products = await db.products.findMany()
return (
<div>
{products.map(product => (
<ProductCard key={product.id} product={product} />
))}
</div>
)
}
Fonksiyon Düzeyinde Önbellekleme
// lib/data.ts
import { cacheLife, cacheTag } from 'next/cache'
export async function getPopularPosts() {
'use cache'
cacheLife('hours')
cacheTag('popular-posts')
const posts = await db.posts.findMany({
orderBy: { views: 'desc' },
take: 10
})
return posts
}
export async function getSiteSettings() {
'use cache'
cacheLife('max') // Nadiren değişir
cacheTag('settings')
return await db.settings.findFirst()
}
Bileşen Düzeyinde Önbellekleme
// components/RecentComments.tsx
import { cacheLife, cacheTag } from 'next/cache'
export default async function RecentComments({ postId }: { postId: string }) {
'use cache'
cacheLife('minutes')
cacheTag(`comments-${postId}`)
const comments = await db.comments.findMany({
where: { postId },
orderBy: { createdAt: 'desc' },
take: 5
})
return (
<section>
<h3>Son Yorumlar</h3>
{comments.map(c => (
<div key={c.id}>{c.text}</div>
))}
</section>
)
}
use cache direktifinin en büyük avantajı, yalnızca fetch ile sınırlı kalmaması. Veritabanı sorguları, dosya sistemi işlemleri, hesaplama ağırlıklı fonksiyonlar... Hepsi önbelleğe alınabiliyor. Bu gerçekten oyun değiştirici bir özellik.
cacheLife: Önbellek Ömrünü Kontrol Etme
cacheLife fonksiyonu, önbelleğe alınan verinin ne kadar süre taze kalacağını belirliyor. Yerleşik profiller kullanabilir ya da kendi özel profillerinizi tanımlayabilirsiniz.
Yerleşik cacheLife Profilleri
import { cacheLife } from 'next/cache'
// Saniyeler — çok kısa ömürlü
cacheLife('seconds')
// Dakikalar — orta ömürlü
cacheLife('minutes')
// Saatler — uzun ömürlü
cacheLife('hours')
// Günler — çok uzun ömürlü
cacheLife('days')
// Haftalık
cacheLife('weeks')
// Maksimum — mümkün olduğunca uzun
cacheLife('max')
Özel cacheLife Profili Tanımlama
Yerleşik profiller işinize yaramıyorsa, kendi profillerinizi tanımlamak oldukça kolay:
// next.config.ts
import type { NextConfig } from 'next'
const nextConfig: NextConfig = {
cacheComponents: true,
cacheLife: {
blog: {
stale: 3600, // 1 saat — bu süre boyunca stale veri kabul edilir
revalidate: 900, // 15 dk — bu süre sonunda arka planda yenileme başlar
expire: 86400, // 1 gün — bu süre sonunda önbellek tamamen silinir
},
urunler: {
stale: 300, // 5 dakika
revalidate: 60, // 1 dakika
expire: 3600, // 1 saat
},
},
}
export default nextConfig
// Kullanım
import { cacheLife } from 'next/cache'
export async function getBlogPosts() {
'use cache'
cacheLife('blog') // Yukarıda tanımlanan özel profil
return await db.posts.findMany()
}
Burada stale, revalidate ve expire arasındaki farkı kavramak gerçekten önemli. Kısaca: stale süresi boyunca eski veri sunulur. revalidate süresinden sonra arka planda yenileme tetiklenir. expire süresinden sonra ise önbellek tamamen silinir ve sonraki istek taze veriyi beklemek zorunda kalır. Bu üçlüyü karıştırmak, üretimde baş ağrısına neden olan hataların en yaygın kaynağı.
cacheTag ile Etiket Tabanlı Önbellek Yönetimi
cacheTag, önbelleğe alınan verileri etiketlemenize ve daha sonra bu etiketlere göre hedefli şekilde geçersiz kılmanıza olanak tanıyor. Özellikle bir içerik değişikliğinin birden fazla sayfayı etkilediği büyük uygulamalarda muazzam fayda sağlıyor.
import { cacheTag, cacheLife } from 'next/cache'
// Tek etiket
export async function getPost(slug: string) {
'use cache'
cacheTag(`post-${slug}`)
cacheLife('hours')
return await db.posts.findUnique({ where: { slug } })
}
// Birden fazla etiket — tek bir girişi birden fazla etiketle ilişkilendirin
export async function getCategoryPosts(categoryId: string) {
'use cache'
cacheTag('posts', `category-${categoryId}`)
cacheLife('hours')
return await db.posts.findMany({ where: { categoryId } })
}
Sınırlar: Bir önbellek girişine en fazla 128 etiket atanabilir ve her etiket en fazla 256 karakter uzunluğunda olabilir. Pratikte bu limitlere ulaşmak zor, ama yine de bilmekte fayda var.
revalidateTag vs updateTag: Doğru API'yi Seçmek
Next.js 16, önbelleği geçersiz kılmak için iki farklı API sunuyor. Hangisini kullanacağınız kullanım senaryonuza bağlı ve bu seçim sandığınızdan daha önemli.
revalidateTag — Stale-While-Revalidate Semantiği
revalidateTag, etiketlenmiş önbellek girişlerini "bayat" (stale) olarak işaretliyor. Sonraki ziyarette eski veri anında sunulurken arka planda taze veri hazırlanıyor. Kullanıcı açısından hiç bekleme yok.
'use server'
import { revalidateTag } from 'next/cache'
export async function publishPost(formData: FormData) {
await db.posts.create({
data: { title: formData.get('title'), /* ... */ }
})
// SWR semantiği ile yenile (önerilen)
revalidateTag('posts', 'max')
}
Önemli: Next.js 16'da revalidateTag ikinci parametre olarak bir cacheLife profili gerektirir. Tek parametreli kullanım artık deprecated — bu yüzden eski kodlarınızı güncellemeyi unutmayın.
updateTag — Anında Geçersiz Kılma (Read-Your-Writes)
updateTag, önbelleği anında siliyor ve sonraki istek taze veriyi bekliyor. Kullanıcının yaptığı değişikliği hemen görmesi gereken senaryolar için tam biçilmiş kaftan.
'use server'
import { updateTag } from 'next/cache'
export async function updateProfile(formData: FormData) {
await db.users.update({
where: { id: formData.get('userId') },
data: { name: formData.get('name') }
})
// Kullanıcı değişikliği anında görsün
updateTag('user-profile')
}
Kısıtlama: updateTag yalnızca Server Actions içinde kullanılabiliyor. Route Handler'larda veya diğer bağlamlarda revalidateTag kullanmanız gerekiyor.
Karşılaştırma Tablosu
| Özellik | revalidateTag | updateTag |
|---|---|---|
| Kullanılabilecek yer | Server Actions ve Route Handlers | Yalnızca Server Actions |
| Davranış | Stale-while-revalidate (SWR) | Anında geçersiz kılma |
| Kullanım senaryosu | Gecikme kabul edilebilir içerik | Kullanıcıya anında yansıması gereken değişiklikler |
| Performans etkisi | Düşük — arka planda yenileme | Yüksek — sonraki istek bloklanır |
use cache Varyantları: private ve remote
use cache: private — Kullanıcıya Özel Önbellekleme
Çerez (cookie) tabanlı kullanıcıya özel verileri önbelleğe almak istiyorsanız, use cache: private tam da bunun için var:
import { cacheTag, cacheLife } from 'next/cache'
import { cookies } from 'next/headers'
async function getRecommendations(productId: string) {
'use cache: private'
cacheTag(`recommendations-${productId}`)
cacheLife({ stale: 60 })
const sessionId = (await cookies()).get('session-id')?.value || 'guest'
return getPersonalizedRecommendations(productId, sessionId)
}
use cache: remote — Paylaşımlı Uzak Önbellek
Serverless ortamlarda tüm instance'ların aynı önbelleği paylaşması gerekiyor mu? use cache: remote tam olarak bunu çözüyor:
async function getExchangeRates(currency: string) {
'use cache: remote'
cacheTag('exchange-rates')
cacheLife({ expire: 3600 })
return await fetchExchangeRates(currency)
}
Üretim Ortamında Sık Karşılaşılan Sorunlar ve Çözümleri
Şimdi gelelim eğlenceli kısma. Üretim ortamında karşılaştığınız sorunlar genellikle önbellekleme katmanlarının beklenmedik şekillerde etkileşmesinden kaynaklanıyor. İşte en yaygın olanlar ve çözümleri:
Sorun 1: Deploy Sonrası Bayat Veri
Uygulamayı deploy ettikten sonra eski verilerin gösterilmesi muhtemelen en yaygın şikayet. Bu genellikle Router Cache kaynaklı.
// Çözüm: Kritik navigasyonlarda router.refresh() kullanın
'use client'
import { useRouter } from 'next/navigation'
export function LoginButton() {
const router = useRouter()
async function handleLogin() {
await signIn()
router.refresh() // Server Component'leri yeniden render et
}
return <button onClick={handleLogin}>Giriş Yap</button>
}
Sorun 2: Layout Persistence ile Login Durumu Güncellenmemesi
Başarılı bir giriş sonrası redirect('/') çalışıyor ama navbar'daki kullanıcı bilgisi güncellenmiyorsa — bu App Router'ın "Layout Persistence" tasarım ilkesinden kaynaklanıyor. Layout bileşenleri rota değişikliklerinde yeniden mount edilmiyor. İlk karşılaştığınızda kafa karıştırıcı olabiliyor ama mantığı var.
// Çözüm: Login action'ından sonra layout'u da revalidate edin
'use server'
import { redirect } from 'next/navigation'
import { revalidatePath } from 'next/cache'
export async function loginAction(formData: FormData) {
await authenticate(formData)
revalidatePath('/', 'layout') // Layout'u da dahil et
redirect('/')
}
Sorun 3: use cache ile Build Timeout
Build sırasında use cache sınırları içinde dinamik veya çalışma zamanı verilerine erişmeye çalışırsanız build askıya alınıyor ve 50 saniye sonra hata veriyor. Bu hatayı ilk kez aldığınızda nedenini anlamak gerçekten zor olabiliyor.
// ❌ YANLIŞ — runtime verisi use cache içinde
async function getData() {
'use cache'
const session = await getSession() // Runtime verisi!
return await db.posts.findMany({ where: { userId: session.userId } })
}
// ✅ DOĞRU — runtime verisini parametre olarak geçirin
async function getData(userId: string) {
'use cache'
cacheTag(`user-posts-${userId}`)
return await db.posts.findMany({ where: { userId } })
}
Kural basit: use cache içinde cookies(), headers() veya session gibi runtime verilerine doğrudan erişmeyin. Bunları parametre olarak geçirin.
Sorun 4: ISR ile Bölgesel Tutarsızlık
Küresel dağıtık ortamlarda (edge network) yeniden oluşturma tüm bölgelerde aynı anda gerçekleşmiyor. Bazı kullanıcılar güncellenmiş içeriği görürken diğerleri eski versiyonu görebilir. Bu aslında beklenen bir davranış, bir bug değil.
Çözüm: Kritik güncellemeler için updateTag kullanarak tüm bölgelerde anında geçersiz kılma yapın. Daha az kritik içerikler için revalidateTag ile SWR yeterli.
Pratik Strateji Tablosu: Hangi Senaryoda Ne Kullanmalı?
Aşağıdaki tablo, farklı senaryolar için hangi önbellekleme stratejisini seçmeniz gerektiğine hızlıca karar vermenize yardımcı olacak:
| Senaryo | Strateji | API / Yapılandırma |
|---|---|---|
| Blog yazıları | Zaman tabanlı ISR | revalidate: 3600 veya cacheLife('hours') |
| Ürün kataloğu | ISR + On-demand | cacheLife('hours') + revalidateTag |
| Kullanıcı profili | Önbellek yok | cache: 'no-store' |
| Dashboard istatistikleri | Kısa revalidation | cacheLife('minutes') |
| CMS içerikleri | On-demand revalidation | cacheTag + updateTag |
| E-ticaret stok durumu | Gerçek zamanlı | cache: 'no-store' |
| Site ayarları | Uzun ömürlü önbellek | cacheLife('max') |
| Arama sonuçları | Parametre bazlı önbellek | use cache + otomatik cache key |
Önbellek Hata Ayıklama (Debug) İpuçları
Geliştirme Ortamında
Geliştirme ortamında önbellek davranışını doğrulamak zor, çünkü Next.js dev modunda birçok önbellek katmanını devre dışı bırakıyor. Bu yüzden üretim ortamını yerel olarak simüle etmeniz gerekiyor:
# Üretim ortamını yerel olarak test edin
next build && next start
# Önbellek debug loglarını etkinleştirin
NEXT_PRIVATE_DEBUG_CACHE=1 next start
Bu komut, hangi önbellek katmanının devreye girdiğini ve hangi verilerin önbellekten sunulduğunu konsola yazdıracak. Sorun giderirken inanılmaz faydalı.
Tarayıcı Tarafında
Network sekmesinde x-nextjs-cache başlığını kontrol edin:
HIT— Sayfa önbellekten sunulduMISS— Sayfa yeni render edildiSTALE— Eski versiyon sunulurken arka planda yenileniyor
Sıkça Sorulan Sorular (FAQ)
Next.js'te ISR ve SSR arasındaki fark nedir?
ISR (Incremental Static Regeneration), sayfayı build zamanında veya ilk istekte oluşturur ve belirli bir süre boyunca önbellekten sunar. Süre dolduğunda arka planda yeni versiyonu hazırlar. SSR (Server-Side Rendering) ise her istekte sayfayı sıfırdan render eder. ISR çok daha hızlı yanıt süreleri sağlarken, SSR her zaman en güncel veriyi sunar. Çoğu senaryo için ISR'ı öncelikli tercih etmenizi tavsiye ederim — performans farkı gerçekten belirgin.
revalidateTag ile updateTag arasındaki farkı nasıl anlamalıyım?
En basit açıklamayla: revalidateTag "arka planda sessizce güncelle", updateTag ise "şimdi hemen güncelle" demek. revalidateTag eski veriyi sunarken arka planda yenisini hazırlıyor. updateTag ise önbelleği anında siliyor ve sonraki istek taze veriyi beklemek zorunda kalıyor. Kullanıcının kendi yaptığı bir değişikliği hemen görmesi gereken durumlarda (mesela profil güncelleme) updateTag, diğer senaryolarda revalidateTag kullanın.
use cache direktifi unstable_cache'in yerini mi alıyor?
Evet, kesinlikle. unstable_cache deneysel bir API olarak kullanımdan kaldırılıyor. Next.js ekibi, use cache direktifine geçiş yapılmasını öneriyor. use cache, yalnızca fetch değil veritabanı sorguları ve diğer sunucu tarafı işlemleri de önbelleğe alabildiği için çok daha güçlü ve esnek.
Neden deploy sonrası eski veriler görünüyor?
Bu sorun genellikle iki kaynaktan doğuyor: Router Cache (istemci tarafı) ve CDN önbelleği. Router Cache, dinamik sayfaları 30 saniye, statik sayfaları 5 dakika boyunca saklıyor. Çözüm olarak kritik navigasyonlarda router.refresh() kullanın ve sunucu tarafında revalidatePath ile ilgili rotaları geçersiz kılın.
Aynı sayfada hem statik hem dinamik içerik olabilir mi?
Evet! Next.js'in Partial Prerendering (PPR) özelliği tam olarak bunu sağlıyor. Sayfanın statik kısımları build zamanında render edilirken, dinamik kısımlar Suspense sınırları içinde sunucudan streaming ile iletiliyor. Ayrıca use cache ile aynı rota içinde farklı bileşenler için farklı önbellek stratejileri tanımlayabilirsiniz — bu esneklik gerçekten etkileyici.