Next.js 16'da use cache: Cache Components ile sunucu tarafı önbellekleme

Next.js 16'da use cache: Cache Components ile sunucu tarafı önbellekleme

use cache artık canary değil — Next.js 16 ile Cache Components'in parçası olarak kararlı sürümde. Nasıl açılır, önbellek anahtarı nasıl üretilir, cookies() neden içeride çalışmaz ve hangi durumda hâlâ unstable_cache gerekir.

Furkan Özay·15 Haziran 2025·9 dk okuma·2 Eyl 2026'de güncellendi
Bu yazı güncellendi

İlk yazdığımda use cache Next.js 15'in canary sürümündeydi ve "production'da kullanma" diyordum. Artık öyle değil: Next.js 16.0 ile Cache Components özelliğinin parçası olarak kararlı sürüme geçti. Yazıyı 16.x dokümantasyonuna göre baştan yazdım — hem doğru kullanım hem de en sık takılınan yerler burada.

Next.js'te sunucu tarafında önbellekleme uzun süre dağınık bir konuydu: fetch çağrıları için ayrı davranış, unstable_cache için ayrı bir API, aynı istek içindeki tekrarlar için React.cache. use cache bu üçünün yerine tek bir direktif koyuyor — bir fonksiyonun, bileşenin ya da tüm bir dosyanın çıktısını önbelleğe alıyorsun ve anahtarı Next.js kendisi üretiyor.

Önce: açmadan çalışmıyor

En sık atlanan adım bu. use cache yazmak tek başına yetmiyor; Cache Components özelliğini açman gerekiyor:

🧪next.config.ts
TypeScript
import type { NextConfig } from "next";
 
const nextConfig: NextConfig = {
	cacheComponents: true,
};
 
export default nextConfig;

Bunu açmadan direktifi yazarsan hiçbir şey önbelleğe alınmaz ve sessizce çalışmaya devam eder — hata da almazsın. Yavaşlığın sebebini ararken en çok zaman kaybettiren şey bu.

Nereye yazılır?

Üç yer var ve üçünde de fonksiyon async olmak zorunda:

🧪Fonksiyon, bileşen ve dosya seviyesi
TSX
// 1. Fonksiyon seviyesi
export async function getData() {
	"use cache";
	const res = await fetch("https://api.example.com/data");
	return res.json();
}
 
// 2. Bileşen seviyesi — prop'lar önbellek anahtarına girer
export async function Bookings({ type = "haircut" }: { type: string }) {
	"use cache";
	const res = await fetch(`https://api.example.com/bookings?type=${type}`);
	return <List items={await res.json()} />;
}
TSX
// 3. Dosya seviyesi — dosyadaki her export önbelleğe alınır
"use cache";
 
export async function getMonthlyTotals(accountId: string) {
	return db.orders.aggregate({ where: { accountId }, _sum: { amount: true } });
}
 
export async function getTopProducts() {
	return db.products.findMany({ orderBy: { sales: "desc" }, take: 10 });
}

Dosya seviyesinde kullanırsan generateMetadata ve generateStaticParams dahil her export kapsama girer; hepsinin async olması gerekir. Bir route segmentini tamamen önceden üretmek istiyorsan direktifi o rotayı oluşturan her dosyanın (page, layout, paralel rota slotları) başına ayrı ayrı koymalısın — her segment bağımsız bir önbellek girdisi.

Önbellek anahtarı nasıl üretiliyor?

Anahtar dört parçadan oluşuyor:

  1. Build ID — her derlemede değişir, yani her dağıtım tüm önbelleği geçersiz kılar
  2. Fonksiyon kimliği — fonksiyonun kod içindeki konumu ve imzasının güvenli özeti
  3. Serileştirilebilir argümanlar — bileşende prop'lar, fonksiyonda parametreler
  4. HMR özeti — yalnız geliştirmede, sıcak yenilemede önbelleği düşürür

Buradaki asıl incelik dördüncü maddede değil, argümanlarda: dış kapsamdan kullandığın değişkenler de otomatik olarak argüman sayılıp anahtara giriyor.

Kapanış (closure) anahtara dahildir
TSX
async function Component({ userId }: { userId: string }) {
	const getData = async (filter: string) => {
		"use cache";
		// Anahtar hem userId'yi (kapanıştan) hem filter'ı (argümandan) içerir
		const res = await fetch(
			`https://api.example.com/users/${userId}/data?filter=${filter}`
		);
		return res.json();
	};
 
	return getData("active");
}

Bu iyi haber: yanlışlıkla bir kullanıcının verisini başkasına göstermiyorsun. Kötü haber: farkında olmadan anahtarı çeşitlendirip önbellek isabet oranını düşürebilirsin. İçeride kullandığın her dış değişkenin anahtarı böldüğünü unutma.

Ne kadar taze kalacak: cacheLife

cacheLife çağırmazsan default profili uygulanır:

  • stale: 5 dakika (istemci tarafı)
  • revalidate: 15 dakika (sunucu tarafı)
  • expire: zamanla hiç dolmaz
💡Ömrü açıkça yaz
TSX
import { cacheLife } from "next/cache";
 
export async function getOrderSummary(accountId: string) {
	"use cache";
	cacheLife("hours");
 
	const orders = await getOrders(accountId);
	return { orders };
}

Dokümantasyonun önerisi net: her use cache kapsamında cacheLife'ı açıkça yaz. Yoksa davranış çağrı yerinde görünmüyor, varsayılana ya da onu saran önbelleğe bağlı kalıyor.

Bir uyarı: cacheLife verilmemiş bir use cache içine kısa ömürlü bir başka use cache yerleştirirsen derleme hata verir. İç içe önbelleklerde ömürleri bilerek seçmen gerekiyor.

Etiketle ve elle temizle: cacheTag

Zamana bağlı tazelenme her zaman doğru cevap değil. İçerik değiştiğinde anında temizlemek istiyorsan etiket kullan:

🧪cacheTag + updateTag
TSX
// lib/products.ts
import { cacheTag } from "next/cache";
 
export async function getProducts() {
	"use cache";
	cacheTag("products");
	const res = await fetch("https://api.example.com/products");
	return res.json();
}
TSX
// app/actions.ts
"use server";
 
import { updateTag } from "next/cache";
 
export async function updateProduct() {
	await db.products.update(/* ... */);
	updateTag("products"); // "products" etiketli tüm girdiler düşer
}

Pratikte ikisi birlikte kullanılıyor: nadiren değişen bir blog yazısı uzun bir cacheLife + cacheTag alıyor ve yalnız kaydedildiğinde temizleniyor; gün içinde akan bir liste ise kısa bir profille kendi kendine tazeleniyor.

En sık takılınan yer: cookies() içeride çalışmaz

Önbelleğe alınmış bir kapsamda cookies(), headers() ve searchParams kullanılamıyor. Üstelik kısıt çağrı yığınını takip ediyor: içeriden çağırdığın bir yardımcı fonksiyon bunlardan birini okuyorsa o da patlıyor (next-request-in-use-cache).

🚨Sinsi tarafı

Dinamik olarak render edilen bir rotada bu hata derlemede değil, çalışma anında ortaya çıkıyor. Yani next build sorunsuz geçip next start altında hata alabilirsin. CI'da yeşil, canlıda kırmızı.

Doğru kalıp: istek verisini önbellek kapsamının dışında oku, içeriye argüman olarak geçir.

💬Yanlış / doğru
TSX
// YANLIŞ — çalışma anında patlar
async function getCart() {
	"use cache";
	const store = await cookies();
	return db.cart.find({ id: store.get("cart")?.value });
}
 
// DOĞRU — okuma dışarıda, argüman içeride
export default async function Page() {
	const store = await cookies();
	const cartId = store.get("cart")?.value;
	return <Cart cartId={cartId} />;
}
 
async function Cart({ cartId }: { cartId?: string }) {
	"use cache";
	return <CartView data={await db.cart.find({ id: cartId })} />;
}

Gerçekten kaçınılmaz olduğu durumlar için use cache: private var, ama dokümantasyon bunu "çok nadiren" diye işaretliyor — uyum zorunluluğu ya da kodu yeniden düzenleyemediğin haller için.

Serileştirme sınırları

Argümanlar ve dönüş değerleri serileştirilebilir olmalı.

Geçerli: ilkel tipler, düz nesneler, diziler, Date, Map, Set, TypedArray, ArrayBuffer. Dönüş değeri ayrıca JSX olabilir.

Geçersiz: sınıf örnekleri, fonksiyonlar, Symbol, WeakMap/WeakSet ve — kolay unutulan — URL örnekleri.

Buradaki kurtarıcı ayrıntı: bir değeri içine bakmadan geçirip kullanabiliyorsun. children ve Server Action'lar böyle çalışıyor.

💡Geçiş (pass-through) kalıbı
TSX
async function CachedWrapper({ children }: { children: ReactNode }) {
	"use cache";
	// children okunmuyor, sadece geçiriliyor — anahtarı etkilemiyor
	return (
		<div className="wrapper">
			<header>Önbelleğe alınmış başlık</header>
			{children}
		</div>
	);
}
 
export default function Page() {
	return (
		<CachedWrapper>
			<DynamicComponent /> {/* önbelleğe alınmıyor */}
		</CachedWrapper>
	);
}

Bu sayede sayfanın statik kabuğunu önbelleğe alıp içine dinamik parçayı yerleştirebiliyorsun. Bir layout'a use cache koyman, altındaki children'ı önbelleğe almaz.

YouTube

Konuyu video anlatımıyla da takip edebilirsin.

Kanala git →

Nerede saklanıyor? Barındırma biçimi sonucu değiştiriyor

Varsayılan işleyici bellek içi LRU. Bu, nerede yayınladığına göre çok farklı davranıyor:

OrtamÇalışma anındaki davranış
ServerlessGirdiler istekler arasında genelde kalmaz; her istek farklı bir örneğe düşebilir. Derleme anındaki önbellekleme normal çalışır.
Kendi sunucunGirdiler istekler arasında kalır. Boyutu cacheMaxMemorySize ile ayarlarsın.
⚠️Sık yapılan yanlış çıkarım

"use cache koydum ama veritabanı yükü düşmedi" şikâyetinin çoğu bu tablodan çıkıyor. Serverless'ta bellek içi önbellek kalıcı değil. Gerçekten paylaşımlı bir önbellek istiyorsan use cache: remote ile ayrı bir işleyici (Redis, KV) bağlaman gerekiyor — bunun da ağ gecikmesi ve platform maliyeti var.

Ayrıca hiçbir önbellek direktifi yeni bir dağıtıma taşınmaz, çünkü anahtar build ID'yi içeriyor. Dağıtımlar arasında kalması gereken veri için hâlâ unstable_cache ya da fetch önbelleği kullanman gerekiyor. use cache her şeyin yerini almıyor.

Derleme 50 saniyede takılıyorsa

Şu hatayı görüyorsan sebebi neredeyse her zaman aynı:

Filling a cache during prerender timed out, likely because request-specific arguments such as params, searchParams, cookies() or uncached data were used inside "use cache".

Önbelleğe alınmış kapsam, derleme sırasında çözülemeyecek bir Promise'i bekliyor. Tipik üç yol: böyle bir Promise'i prop olarak geçirmek, kapanıştan erişmek, ya da paylaşılan bir Map üzerinden almak. Çözüm aynı: değeri dışarıda await et, içeriye düz veri geçir.

cookies()'i doğrudan içeride çağırmak ise zaman aşımı değil, anında hata verir — ikisini karıştırma.

Bilmen gereken diğer iki davranış

Taslak modu (Draft Mode) açıkken tüm önbelleğe alınmış fonksiyonlar her istekte yeniden çalışır ve sonuç kaydedilmez. Önizlemede bayat içerik görme riski yok; bunun için ekstra kod yazmana gerek kalmıyor.

React.cache yalıtılmıştır. use cache sınırının dışında React.cache ile sakladığın bir değer içeriden görünmez. Veriyi içeri sokmanın tek yolu argüman.

Ne zaman kullanmamalı?

  • Statik dışa aktarımda (output: "export") desteklenmiyor. Node.js sunucusu ve Docker destekli.
  • Kullanıcıya özel, her istekte değişen veride — kapsam dışına çıkarıp argüman geçirmen gerekir, o da çoğu zaman önbelleği anlamsız kılar.
  • Dağıtımlar arasında yaşaması gereken pahalı hesaplamalarda; build ID anahtarı sıfırlıyor.
  • Verinin tazeliği saniyelik önem taşıyorsa: istemci yönlendiricisi en az 30 saniyelik bir bayatlık süresi dayatıyor, yapılandırmadan bağımsız olarak.

Hata ayıklama

💡Ayrıntılı günlük
Bash
NEXT_PRIVATE_DEBUG_CACHE=1 npm run dev
# ya da üretim için
NEXT_PRIVATE_DEBUG_CACHE=1 npm run start

Geliştirmede önbelleğe alınmış fonksiyonlardan gelen console.log çıktıları Cache önekiyle görünüyor — hangi çağrının önbellekten döndüğünü anlamanın en hızlı yolu.

Özet

📋Kısa hâli
  • use cache Next.js 16.0'da kararlı; cacheComponents: true ile açılıyor
  • Fonksiyon, bileşen, dosya ve route segmenti seviyesinde çalışıyor; hepsi async olmalı
  • Anahtar: build ID + fonksiyon kimliği + argümanlar + kapanıştan yakalanan değişkenler
  • cacheLife ile ömür, cacheTag + updateTag ile elle temizleme
  • cookies(), headers(), searchParams içeride yasak — dışarıda oku, argüman geçir
  • Varsayılan depolama bellek içi LRU; serverless'ta istekler arasında kalmaz
  • Dağıtımlar arası kalıcılık gerekiyorsa hâlâ unstable_cache ya da fetch önbelleği

Bu yazının ilk hâlinde bir "performans testi" bölümü vardı; %78 hızlanma gibi sayılar veriyordu. Onu kaldırdım, çünkü o sayılar gerçek bir ölçümden değil uydurma bir setTimeout kurgusundan geliyordu ve senin projendeki kazancı tahmin etmene yaramıyordu. Kazanç tamamen neyi önbelleğe aldığına bağlı: 200 ms süren bir veritabanı sorgusu ile 2 ms süren bir hesaplamayı önbelleğe almak aynı şey değil. Kendi ölçümünü NEXT_PRIVATE_DEBUG_CACHE=1 ile yap.

📝Kaynak

Bu yazı Next.js 16.x resmî dokümantasyonuna (use cache direktifi) dayanıyor. Sürüm geçmişi: v15.0.0 deneysel olarak tanıttı, v16.0.0 Cache Components ile kararlı hâle getirdi.

Bu yazıyı faydalı bulduysan paylaşabilirsin.

Furkan Özay

Yazılım geliştirici ve eğitmen; Next.js, React ve altyapı üzerine yazıyor.

czay.dev

Bunu da oku

Yorumlar