Next.js Caching ve ISR Stratejileri: Performance Optimizasyonu Rehberi
Next.js 15'te caching mekanizmalarını ve Incremental Static Regeneration stratejilerini öğrenin. Request memoization, data cache, full route cache ve revalidation teknikleri bu kapsamlı rehberde.
Next.js Caching ve ISR Stratejileri: Performance Optimizasyonu Rehberi
Performance, modern web uygulamalarının en kritik metriklerinden biri. Kullanıcılar hızlı yüklenen sayfalar bekliyor, arama motorları Core Web Vitals'ı sıralama faktörü olarak kullanıyor. Next.js'in caching sistemi, doğru kullanıldığında dramatik performance kazanımları sağlıyor.
Next.js 15 ile caching davranışları değişti. Artık fetch istekleri varsayılan olarak cache'lenmiyor. Bu rehberde yeni caching modelini, ISR stratejilerini ve on-demand revalidation tekniklerini derinlemesine inceleyeceğiz.
İçindekiler
- Next.js 15 Caching Değişiklikleri
- Caching Katmanları
- Request Memoization
- Data Cache
- Full Route Cache
- Incremental Static Regeneration (ISR)
- On-Demand Revalidation
- Route Segment Config
- Streaming ve Partial Prerendering
- Cache Debugging
- Production Best Practices
- Sık Sorulan Sorular
Next.js 15 Caching Değişiklikleri
Next.js 15, önceki sürümlerden önemli farklılıklar içeriyor. En büyük değişiklik: fetch istekleri artık varsayılan olarak cache'lenmiyor.
Önceki Davranış (Next.js 14)
// Varsayılan: cache: 'force-cache'
const data = await fetch('https://api.example.com/data')
// Bu istek cache'lenirdiYeni Davranış (Next.js 15)
// Varsayılan: cache: 'no-store'
const data = await fetch('https://api.example.com/data')
// Bu istek her request'te yeniden yapılır
// Cache istemek için explicit olarak belirtmeli
const cachedData = await fetch('https://api.example.com/data', {
cache: 'force-cache'
})Neden Bu Değişiklik?
- Predictability: Geliştiriciler caching davranışını explicit olarak kontrol ediyor
- Freshness: Varsayılan olarak güncel data
- Debugging: Cache sorunları daha kolay tespit ediliyor
- Clarity: Hangi verinin cache'lendiği açık
Caching Katmanları
Next.js'te dört ana caching katmanı var:
| Katman | Konum | Amaç | Süre |
|---|---|---|---|
| Request Memoization | Server | Aynı request'te duplicate fetch önleme | Request lifecycle |
| Data Cache | Server | fetch() sonuçlarını cache'leme | Persistent |
| Full Route Cache | Server | Rendered route'ları cache'leme | Persistent |
| Router Cache | Client | Visited route'ları cache'leme | Session |
Request → Router Cache (Client)
↓
Full Route Cache (Server)
↓
Data Cache (Server)
↓
Request Memoization (Server)
↓
Data Source (Database/API)
Request Memoization
Request memoization, aynı render pass'ta yapılan aynı fetch isteklerini otomatik olarak deduplicate eder.
Nasıl Çalışır?
// app/page.tsx
async function getUser(id: string) {
// Bu istek memoize edilir
const res = await fetch(`https://api.example.com/users/${id}`)
return res.json()
}
export default async function Page() {
const user1 = await getUser('1') // Fetch yapılır
const user2 = await getUser('1') // Memoized sonuç döner
// Sadece 1 network request yapılır!
return <div>{user1.name}</div>
}Layout ve Page Arası Paylaşım
// app/layout.tsx
export default async function Layout({ children }) {
const user = await getUser('1') // Fetch yapılır
return (
<div>
<Header user={user} />
{children}
</div>
)
}
// app/page.tsx
export default async function Page() {
const user = await getUser('1') // Memoized! Network request yok
return <Profile user={user} />
}Önemli Notlar
- Sadece GET metodlu fetch istekleri memoize edilir
- POST, PUT, DELETE memoize edilmez
- Sadece React component tree içinde çalışır
- Route handlers'da çalışmaz
- Request bittiğinde cache temizlenir
Manuel Memoization
import { cache } from 'react'
// React cache ile custom fonksiyonları memoize et
export const getUser = cache(async (id: string) => {
const user = await db.user.findUnique({ where: { id } })
return user
})
// Birden fazla component'ta çağrılsa bile tek queryData Cache
Data cache, fetch() sonuçlarını request'ler arası persistent olarak saklar.
Cache Seçenekleri
// Cache'leme yok (Next.js 15 varsayılan)
fetch('https://api.example.com/data', { cache: 'no-store' })
// Süresiz cache
fetch('https://api.example.com/data', { cache: 'force-cache' })
// Time-based revalidation
fetch('https://api.example.com/data', {
next: { revalidate: 3600 } // 1 saat
})
// Tag-based cache
fetch('https://api.example.com/data', {
next: { tags: ['products'] }
})Revalidate Seçenekleri
// Hiç revalidate etme (static)
next: { revalidate: false }
// Her request'te revalidate (dynamic)
next: { revalidate: 0 }
// 60 saniyede bir revalidate
next: { revalidate: 60 }Stale-While-Revalidate
Next.js, stale-while-revalidate pattern kullanır:
- İlk request → Data fetch edilir ve cache'lenir
- Sonraki requestler → Cache'ten serve edilir
- Revalidate süresi dolunca → Background'da yeni data fetch edilir
- Yeni data hazır → Cache güncellenir
// Örnek: 60 saniye revalidate
const products = await fetch('https://api.example.com/products', {
next: { revalidate: 60 }
})
// 0-60 saniye: Cache'ten serve
// 60+ saniye: Stale data serve, background'da refresh
// Refresh tamamlandığında: Yeni data cache'e yazılırFull Route Cache
Full Route Cache, build time veya revalidation sırasında rendered route'ları cache'ler.
Static vs Dynamic Routes
// Static Route - Build time'da render edilir
// app/about/page.tsx
export default function AboutPage() {
return <div>Hakkımızda</div>
}
// → Full Route Cache'e alınır
// Dynamic Route - Her request'te render edilir
// app/dashboard/page.tsx
export default async function DashboardPage() {
const user = await getCurrentUser() // cookies() kullanıyor
return <div>Hoşgeldin {user.name}</div>
}
// → Cache'lenmezDynamic Functions
Aşağıdaki fonksiyonlar kullanıldığında route otomatik dynamic olur:
import { cookies, headers } from 'next/headers'
import { searchParams } from 'next/navigation'
// cookies() - Dynamic
const session = (await cookies()).get('session')
// headers() - Dynamic
const userAgent = (await headers()).get('user-agent')
// searchParams - Dynamic
export default function Page({ searchParams }) {
const query = searchParams.q
}Route'u Static Yapma
// Route segment config ile
export const dynamic = 'force-static'
export const revalidate = 3600
export default async function Page() {
// Bu route static olarak cache'lenir
const data = await fetchData()
return <div>{data}</div>
}Incremental Static Regeneration (ISR)
ISR, static sayfaları belirli aralıklarla veya on-demand olarak yeniden oluşturmanızı sağlar.
Time-Based ISR
// app/blog/[slug]/page.tsx
// Her 60 saniyede bir revalidate
export const revalidate = 60
export default async function BlogPost({ params }) {
const post = await getPost(params.slug)
return <article>{post.content}</article>
}
// Static generation için
export async function generateStaticParams() {
const posts = await getAllPosts()
return posts.map(post => ({ slug: post.slug }))
}ISR Akışı
1. Build Time
→ generateStaticParams() çağrılır
→ Her slug için sayfa render edilir
→ HTML + RSC payload cache'lenir
2. Request (0-60 sn)
→ Cache'ten serve edilir
→ Anında response
3. Request (60+ sn)
→ Stale cache serve edilir
→ Background'da regeneration başlar
4. Regeneration Tamamlandığında
→ Yeni HTML + RSC payload cache'e yazılır
→ Sonraki requestler yeni içeriği görür
Fetch-Level Revalidation
export default async function ProductsPage() {
// Her 5 dakikada bir revalidate
const products = await fetch('https://api.example.com/products', {
next: { revalidate: 300 }
})
// Her 1 saatte bir revalidate
const categories = await fetch('https://api.example.com/categories', {
next: { revalidate: 3600 }
})
// Route'un revalidation süresi en kısa olan (5 dk) olur
return <ProductList products={products} categories={categories} />
}On-Demand Revalidation
On-demand revalidation, time-based yerine event-driven cache invalidation sağlar.
revalidatePath
// app/actions/posts.ts
'use server'
import { revalidatePath } from 'next/cache'
export async function createPost(data: PostInput) {
await db.post.create({ data })
// Belirli path'i revalidate et
revalidatePath('/blog')
// Nested path'leri de dahil et
revalidatePath('/blog', 'layout')
// Dynamic segment
revalidatePath('/blog/[slug]', 'page')
}
export async function updatePost(id: string, data: PostInput) {
const post = await db.post.update({ where: { id }, data })
// Spesifik post sayfasını revalidate et
revalidatePath(`/blog/${post.slug}`)
// Liste sayfasını da revalidate et
revalidatePath('/blog')
}revalidateTag
// Data fetching with tags
async function getProducts() {
const res = await fetch('https://api.example.com/products', {
next: { tags: ['products'] }
})
return res.json()
}
async function getProduct(id: string) {
const res = await fetch(`https://api.example.com/products/${id}`, {
next: { tags: ['products', `product-${id}`] }
})
return res.json()
}
// Revalidation
'use server'
import { revalidateTag } from 'next/cache'
export async function updateProduct(id: string, data: ProductInput) {
await db.product.update({ where: { id }, data })
// Sadece bu ürünü revalidate et
revalidateTag(`product-${id}`)
}
export async function refreshAllProducts() {
// Tüm ürünleri revalidate et
revalidateTag('products')
}Webhook ile Revalidation
// app/api/revalidate/route.ts
import { revalidatePath, revalidateTag } from 'next/cache'
import { NextRequest, NextResponse } from 'next/server'
export async function POST(request: NextRequest) {
const secret = request.headers.get('x-revalidate-secret')
if (secret !== process.env.REVALIDATE_SECRET) {
return NextResponse.json({ error: 'Invalid secret' }, { status: 401 })
}
const body = await request.json()
try {
if (body.type === 'product.updated') {
revalidateTag(`product-${body.productId}`)
revalidatePath('/products')
}
if (body.type === 'post.published') {
revalidatePath('/blog')
revalidatePath(`/blog/${body.slug}`)
}
return NextResponse.json({ revalidated: true })
} catch (error) {
return NextResponse.json({ error: 'Revalidation failed' }, { status: 500 })
}
}Route Segment Config
Route segment config ile caching davranışını dosya seviyesinde kontrol edebilirsiniz.
Tüm Seçenekler
// app/products/page.tsx
// Dynamic behavior
export const dynamic = 'auto' // default
// 'auto' - Next.js karar verir
// 'force-dynamic' - Her zaman dynamic
// 'force-static' - Her zaman static
// 'error' - Dynamic olursa hata ver
// Revalidation süresi
export const revalidate = false // default
// false - Süresiz cache (static)
// 0 - Her zaman revalidate (dynamic)
// number - Saniye cinsinden süre
// Fetch cache davranışı
export const fetchCache = 'auto' // default
// 'auto' - Framework karar verir
// 'default-cache' - Varsayılan cache
// 'only-cache' - Sadece cache'ten
// 'force-cache' - Her zaman cache
// 'default-no-store' - Varsayılan no-store
// 'only-no-store' - Sadece fresh data
// 'force-no-store' - Her zaman fresh
// Runtime
export const runtime = 'nodejs' // default
// 'nodejs' - Node.js runtime
// 'edge' - Edge runtime
// Max duration (saniye)
export const maxDuration = 60Layout'ta Config
// app/dashboard/layout.tsx
export const dynamic = 'force-dynamic'
// Bu layout ve altındaki tüm sayfalar dynamic olur
export default function DashboardLayout({ children }) {
return <div>{children}</div>
}Kombinasyonlar
// Static blog sayfası - 1 saatte bir revalidate
export const revalidate = 3600
export const dynamic = 'force-static'
// Real-time dashboard - her zaman fresh
export const dynamic = 'force-dynamic'
export const fetchCache = 'force-no-store'
// E-commerce ürün listesi - 5 dakikada bir
export const revalidate = 300Streaming ve Partial Prerendering
Streaming with Suspense
// app/dashboard/page.tsx
import { Suspense } from 'react'
export default function DashboardPage() {
return (
<div>
{/* Hemen render edilir */}
<h1>Dashboard</h1>
{/* Stream edilir */}
<Suspense fallback={<StatsSkeleton />}>
<Stats />
</Suspense>
<Suspense fallback={<ChartSkeleton />}>
<RevenueChart />
</Suspense>
<Suspense fallback={<TableSkeleton />}>
<RecentOrders />
</Suspense>
</div>
)
}
// Her component kendi hızında yüklenir
async function Stats() {
const stats = await fetchStats() // 100ms
return <StatsDisplay stats={stats} />
}
async function RevenueChart() {
const revenue = await fetchRevenue() // 500ms
return <Chart data={revenue} />
}
async function RecentOrders() {
const orders = await fetchOrders() // 300ms
return <OrdersTable orders={orders} />
}Partial Prerendering (Experimental)
// next.config.js
module.exports = {
experimental: {
ppr: true
}
}
// app/product/[id]/page.tsx
import { Suspense } from 'react'
export default async function ProductPage({ params }) {
// Static shell - build time'da render edilir
const product = await getProduct(params.id)
return (
<div>
{/* Static */}
<h1>{product.name}</h1>
<ProductImages images={product.images} />
<ProductDescription description={product.description} />
{/* Dynamic - request time'da render edilir */}
<Suspense fallback={<PriceSkeleton />}>
<DynamicPrice productId={params.id} />
</Suspense>
<Suspense fallback={<StockSkeleton />}>
<StockStatus productId={params.id} />
</Suspense>
</div>
)
}
// Fiyat ve stok durumu her zaman güncel
async function DynamicPrice({ productId }) {
const price = await fetchCurrentPrice(productId)
return <Price value={price} />
}Cache Debugging
Logging
// next.config.js
module.exports = {
logging: {
fetches: {
fullUrl: true
}
}
}
// Console output:
// GET https://api.example.com/products 200 in 45ms (cache: HIT)
// GET https://api.example.com/user 200 in 120ms (cache: MISS)Cache Headers
// Response headers'ı kontrol et
export async function GET() {
const data = await fetchData()
return NextResponse.json(data, {
headers: {
'Cache-Control': 'public, s-maxage=60, stale-while-revalidate=30',
'X-Cache-Status': 'HIT' // veya 'MISS'
}
})
}Debug Mode
// Development'ta cache'i devre dışı bırak
const data = await fetch('https://api.example.com/data', {
cache: process.env.NODE_ENV === 'development' ? 'no-store' : 'force-cache'
})Production Best Practices
1. Stratejik Cache Süreleri
// Nadiren değişen içerik - uzun cache
export const revalidate = 86400 // 24 saat
// Orta sıklıkta değişen - orta cache
export const revalidate = 3600 // 1 saat
// Sık değişen - kısa cache
export const revalidate = 60 // 1 dakika
// Real-time gereken - cache yok
export const dynamic = 'force-dynamic'2. Tag-Based Invalidation
// Granular cache control
async function getProducts(categoryId?: string) {
const tags = ['products']
if (categoryId) {
tags.push(`category-${categoryId}`)
}
return fetch('/api/products', {
next: { tags }
})
}
// Sadece ilgili cache'i invalidate et
revalidateTag(`category-${categoryId}`)3. Cascade Revalidation
// Ürün güncellendiğinde
async function updateProduct(id: string, data: ProductInput) {
const product = await db.product.update({ where: { id }, data })
// Ürün sayfası
revalidatePath(`/products/${product.slug}`)
// Kategori sayfası
revalidatePath(`/categories/${product.categorySlug}`)
// Ana sayfa (öne çıkan ürünler)
revalidateTag('featured-products')
// Sitemap
revalidatePath('/sitemap.xml')
}4. Edge Caching
// Vercel Edge Config
// vercel.json
{
"headers": [
{
"source": "/api/products",
"headers": [
{
"key": "Cache-Control",
"value": "s-maxage=60, stale-while-revalidate=300"
}
]
}
]
}5. Static Generation Priority
// Mümkün olduğunca static generate et
export async function generateStaticParams() {
// En popüler 1000 ürünü static generate et
const products = await db.product.findMany({
orderBy: { viewCount: 'desc' },
take: 1000,
select: { slug: true }
})
return products.map(p => ({ slug: p.slug }))
}
// Diğerleri on-demand generate edilir
export const dynamicParams = true6. Monitoring
// Cache hit rate tracking
import { headers } from 'next/headers'
export default async function Page() {
const headersList = await headers()
const cacheStatus = headersList.get('x-vercel-cache')
// Analytics'e gönder
if (cacheStatus === 'MISS') {
trackCacheMiss('/page-path')
}
return <div>...</div>
}Sık Sorulan Sorular
Next.js 15'te neden cache varsayılan olarak kapalı?
Predictability ve debugging kolaylığı için. Geliştiriciler caching davranışını explicit olarak kontrol etmeli.
ISR ile on-demand revalidation arasındaki fark nedir?
ISR time-based (örn: her 60 saniyede), on-demand event-based (örn: CMS'te içerik güncellendiğinde).
revalidatePath tüm cache'i temizler mi?
Hayır, sadece belirtilen path'in cache'ini temizler. Tag-based revalidation daha granular kontrol sağlar.
Edge runtime'da caching nasıl çalışır?
Edge'de Data Cache yoktur. Request memoization çalışır. ISR için Node.js runtime gerekir.
Client-side cache (Router Cache) ne kadar sürer?
Prefetch edilen dynamic route'lar 30 saniye, static route'lar 5 dakika cache'lenir.
generateStaticParams ile kaç sayfa generate edebilirim?
Teknik limit yok ama build time artar. Öncelikle yüksek trafikli sayfaları generate edin.
Sonuç
Next.js'in caching sistemi, doğru kullanıldığında dramatik performance iyileştirmeleri sağlıyor. ISR ile static site generation'ın hızını, dynamic rendering'in esnekliğiyle birleştirebilirsiniz.
Next.js 15'in "opt-in caching" yaklaşımı başlangıçta farklı gelebilir ama uzun vadede daha predictable ve debug edilebilir uygulamalar ortaya çıkarıyor.
Sonraki Adımlar:
- Mevcut fetch'lerinize explicit cache options ekleyin
- Kritik sayfalar için ISR implement edin
- CMS webhook'larıyla on-demand revalidation kurun
- Cache hit rate'inizi monitör edin
- Tag-based invalidation stratejisi oluşturun
Serinin son bölümünde Next.js uygulamalarını Vercel'e deploy etmeyi ve production optimizasyonlarını işleyeceğiz.
Projenizi Hayata Geçirelim
Web sitesi, mobil uygulama veya yapay zeka çözümü mü arıyorsunuz? Fikirlerinizi birlikte değerlendirelim.
Ücretsiz Danışmanlık Alın