Next.js App Router'da sunucu ve istemci bileşenleri, önbellek ve revalidate
App Router'a geçen hemen herkes aynı iki soruda takılıyor: bu bileşene "use client" yazmalı mıyım ve sayfam neden her istekte yeniden oluşturuluyor (ya da tam tersi, neden verim güncellenmiyor)? İkisinin cevabı da aynı modelden çıkıyor, ama modelin önbellek kısmı son üç ana sürümde epey değişti.
Bu yazıda portföyümdeki iki Next.js projesinden, kahve e-ticaret sitesinden ve klinik yönetim panelinden örneklerle ilerliyorum. İkisi de Next.js 14.2 ile yazıldı. Eylül 2026 itibarıyla güncel sürüm 16 serisi, bu yüzden sürüme bağlı her detayda hangi sürümden söz edildiği belirtiliyor.
Varsayılan sunucu, "use client" bir sınır
App Router'da her bileşen varsayılan olarak sunucu bileşenidir. Sunucuda çalışır, veritabanına doğrudan erişebilir ve tarayıcıya JavaScript'i gönderilmez. "use client" yazdığınızda o dosya ve içe aktardığı her şey istemci paketine girer.
İstemci bileşenine yalnızca şu durumlarda ihtiyacınız var:
useState,useEffectgibi hook'laronClick,onChangegibi olay dinleyicileriwindow,localStoragegibi tarayıcı API'leri- Bunları kullanan kütüphaneler (grafik kütüphaneleri, istemci durum depoları)
Kahve e-ticaret sitesinde 9 dosyada "use client" var: sepet çekmecesi, gezinme çubuğu, sepete ekle kutusu, ödeme formu, "kahveni bul" testi, abonelik formu, sepet ve sipariş takip sayfaları, bir de Zustand sepet deposu. Klinik yönetim panelinde 12 dosya: sürükle bırak takvim, Recharts grafikleri, formlar ve uygulama kabuğu gibi etkileşimli parçalar. Geri kalan her şey sunucu bileşeni.
Ürün sayfası bu ayrımı iyi gösteriyor. Sayfanın kendisi sunucuda ürünü veritabanından okuyor, yalnızca gramaj ve öğütüm seçilen kutu istemci bileşeni:
// app/urun/[slug]/page.tsx (sunucu bileşeni)
export default function Page({ params }: { params: { slug: string } }) {
const p = getProduct(params.slug); // doğrudan SQLite sorgusu
if (!p) notFound();
return (
<section>
<h1>{p.name}</h1>
<AddToCart p={p} /> {/* "use client" olan tek parça */}
</section>
);
}
Kural: "use client" satırını olabildiğince yaprağa itin. Bir sayfanın ya da düzenin en üstüne yazarsanız bütün ağaç istemciye taşınır ve sunucuda veri çekmenin avantajı kaybolur.
İki ayrıntıya dikkat edin. Sunucudan istemci bileşenine geçen prop'lar serileştirilebilir olmalı: düz nesne, dizi, metin, sayı geçer, fonksiyon (Server Action hariç) ve sınıf örneği geçmez. Ayrıca veritabanı ya da gizli anahtar içeren bir modülün yanlışlıkla istemci bileşenine aktarılmasını önlemek için o modülün başına import "server-only" eklemek iyi bir sigorta.
Veriyi nerede çekmeli
Çoğu zaman cevap: onu kullanan sunucu bileşeninde. Sunucu bileşeni async olabilir ve veritabanını doğrudan sorgulayabilir. Yalnızca kendi arayüzünüz için bir API rotası yazıp sonra onu fetch etmek, App Router'da genelde gereksiz bir ara katman.
Birbirinden bağımsız sorguları sırayla await etmeyin, Promise.all ile paralel başlatın. Yavaş bir parça varsa onu <Suspense> ile sarın ya da klasöre loading.tsx koyun. Sayfanın geri kalanı hemen gelir, yavaş kısım hazır olunca akar.
İstemcide veri çekmeyi, gerçekten istemcide olması gereken durumlara bırakmak daha doğru: yazdıkça sonuç gösteren arama, periyodik yenilenen bir sayaç gibi.
Önbellek: sürüme göre ne değişti
Karışıklığın büyük kısmı buradan geliyor, çünkü internetteki örnekler farklı sürümlere ait.
| Konu | Next.js 14 | Next.js 15 | Next.js 16 |
|---|---|---|---|
fetch | Varsayılan olarak önbellekte | Varsayılan olarak önbelleksiz | 15 ile aynı. Cache Components açıksa önbellek "use cache" ile seçilir |
GET Route Handler | Varsayılan olarak önbellekte | Önbelleksiz | Önbelleksiz. Cache Components açıksa sayfalarla aynı kurallar |
cookies(), headers(), params | Eşzamanlı | Asenkron, eşzamanlı kullanım uyarı veriyor | Yalnızca asenkron (await cookies()) |
| Etiketle yenileme | revalidateTag(tag) | revalidateTag(tag) | revalidateTag(tag, "max") ikinci argüman gerekli, yeni updateTag(tag) |
Next.js 16'daki büyük yenilik Cache Components. next.config.ts içinde cacheComponents: true ile açılıyor ve isteğe bağlı. Açıldığında her şey varsayılan olarak istek anında çalışıyor, önbelleğe almak istediğiniz sayfa, bileşen ya da fonksiyonu "use cache" ile işaretliyorsunuz, süresini cacheLife("hours") gibi profillerle, etiketini cacheTag("urunler") ile veriyorsunuz. Bu modda export const dynamic ve export const revalidate gibi rota ayarları hata veriyor, yerlerini "use cache" ve cacheLife alıyor. Açmadığınız sürece önceki model geçerli.
Önceki modelin (14 ve 15, 16'da Cache Components kapalıyken) mantığı şu: rota, istek anı verisi kullanmıyorsa derleme sırasında statik olarak üretilir. Bunun beni ilk yakaladığı yer veritabanı sorgularıydı. Kahve e-ticaret sitesinin ana sayfası ve ürün sayfaları SQLite'ı doğrudan okuyor, fetch yok, cookies() yok. Next.js 14 böyle bir sayfayı bir kez üretip saklar (sabit sayfaları derlemede, [slug] gibi dinamik segmentleri ilk istekte). Öne çıkan ürünler ve stok, üretildiği andaki hâliyle donar. Bu yüzden veritabanı okuyan sayfalarda export const dynamic = "force-dynamic" kullandım. Bedeli her istekte bir veritabanı sorgusu. Küçük bir mağazada sorun değil, büyük katalogda zamana bağlı yenileme daha mantıklı olur.
Yeniden doğrulama
İki yol var ve çoğu projede ikisi birlikte kullanılıyor.
Zamana bağlı yenileme: önceki modelde export const revalidate = 3600, Cache Components ile cacheLife("hours"). Blog yazısı, kampanya listesi gibi birkaç dakika eski olması sorun olmayan içerik için.
Olaya bağlı yenileme: veri değiştiği anda ilgili sayfayı ya da etiketi geçersiz kılmak. Kahve e-ticaret sitesinin yönetim panelinde stok güncellenince hem panel hem vitrin yenileniyor:
"use server";
export async function setStock(id: number, stock: number) {
if (!(await isAdmin())) return;
db.update(schema.products).set({ stock: Math.max(0, stock) })
.where(eq(schema.products.id, id)).run();
revalidatePath("/admin/urunler");
revalidatePath("/urunler");
}
Klinik yönetim panelinde bir randevu taşındığında takvim, özet ve hatırlatmalar sayfaları yenileniyor. Next.js 16'da etiket kullanıyorsanız fark şurada: updateTag yalnızca Server Action içinde çalışıyor ve kullanıcının kendi yaptığı değişikliği hemen görmesini sağlıyor. revalidateTag(tag, "max") ise eski veriyi sunup arka planda yeniliyor, webhook gibi yerlere uygun. Ayrıca 15'ten beri render sırasında revalidatePath ya da revalidateTag çağırmak hata veriyor. Bunlar yalnızca Server Action ve Route Handler içinde çağrılmalı.
Sık yapılan hatalar
Düzende çerez okumak. Önceki modelde cookies() ya da headers() okuyan her bileşen, bulunduğu rotayı tamamen dinamik yapar. Kök düzende tema ya da dil çerezi okursanız sitenin tüm sayfaları statik olmaktan çıkar. Klinik yönetim panelinin düzeni seçili şubeyi çerezden okuyor ve bu bilinçli: oturum isteyen bir panelin zaten her istekte çalışması gerekiyor. Aynı satır bir tanıtım sitesinin kök düzeninde olsaydı her sayfayı gereksiz yere sunucuda üretirdi. Çözüm çerezi okuyan kısmı küçültmek: seçimi istemci bileşeninde yapmak ya da Cache Components açıksa çerezi okuyan bileşeni <Suspense> ile sarmak. Bu durumda sayfanın geri kalanı statik kabuk olarak önceden üretilir, yalnızca o parça istek anında gelir.
"use cache" içinde cookies() çağırmak. Önbelleğe alınan fonksiyon istek anı API'lerine erişemez, çağırırsanız hata alırsınız. Çerezi dışarıda okuyun, değeri argüman olarak geçin. Argüman önbellek anahtarının parçası olur.
Server Action'ları gizli sanmak. "use server" ile dışa aktardığınız her fonksiyon, dışarıdan çağrılabilen bir HTTP uç noktasıdır. Formun yalnızca yöneticiye gösterilmesi yetmez. Kahve e-ticaret sitesindeki her yönetim eyleminin ilk satırı bu yüzden yetki kontrolü.
"use client" satırını en üste yazmak. Tek bir onClick için bütün sayfayı istemciye taşımak yerine, butonu ayrı bir bileşene çıkarın.
Sürüm geçişinde await unutmak. 15'e geçerken cookies() ve params asenkron oldu, 16'da eşzamanlı kullanım tamamen kaldırıldı. Resmi codemod işin çoğunu yapıyor: npx @next/codemod@canary next-async-request-api .
Kısa kontrol listesi
"use client"yalnızca etkileşimli yapraklarda mı?- Veritabanı kodu
server-onlyile korunuyor mu? - Bağımsız sorgular paralel mi, yavaş kısımlar
<Suspense>içinde mi? next buildçıktısında hangi rotaların statik, hangilerinin dinamik olduğunu kontrol ettiniz mi?- Veriyi değiştiren her Server Action, etkilediği sayfaları ya da etiketleri yeniliyor mu ve önce yetki kontrolü yapıyor mu?
- Hangi Next.js sürümündesiniz ve okuduğunuz örnek o sürüme mi ait?
Mevcut bir Next.js projesini 16'ya taşıyacaksanız ya da önbellek davranışı kafanızı karıştırıyorsa bana yazabilirsiniz.