fetch Cache'ini Anlamadan Production'a Çıkmak
force-cache, no-store ve tag aynı cümlede değil

Destek kuyruğuna 'fiyat yanlış' diye üç ticket düştü. Staging'de kampanya fiyatı anında görünüyordu. Production'da vitrin hâlâ eski tutarı basıyordu. API doğru, admin panel doğru, CDN'i suçladık. Suçlu CDN değildi. fetch, Next'in data cache'inde ürüne kilitlenmişti. Staging'de her istekte sunucuyu yeniden başlatıyorduk; cache ısınıyordu. Production ısınmış kaldı.
App Router'da fetch üç kapıdan birine girer: zorla tut, hiç tutma, belirli süre veya tag ile tut. Sürümler arasında varsayılan değişti. 14'te GET çoğu yerde cache'lenirdi, 15'te no-store'a kaydı. Ezber 'fetch cache'ler' veya 'fetch cache'lemez' ikisi de eksik. Anahtar, ikinci argüman ve route'un dinamik olup olmadığı.
Üç kapı
force-cache, yanıtı data cache'e yazar. Aynı URL, aynı başlıklar, aynı metot — hit. no-store, her istekte kaynağa iner. next.revalidate saniye verir; süre dolunca arka planda tazelenir. next.tags bir etiket yapıştırır; revalidateTag o etiketi düşürür. Üçünü aynı fetch'e yığmak, hangisinin kazandığını tahmine bırakır.
import { revalidateTag } from "next/cache";
export async function getOffer(slug: string) {
const response = await fetch(`${process.env.PRICING_URL}/offers/${slug}`, {
next: { revalidate: 60, tags: [`offer:${slug}`] },
});
if (!response.ok) throw new Error("offer-unavailable");
return response.json() as Promise<{ slug: string; cents: number }>;
}
export async function publishOffer(slug: string) {
await writeOffer(slug);
revalidateTag(`offer:${slug}`, "max");
}
Kampanya fiyatı için 60 saniye kabul edilebilir bir gecikmeydi; ticket'lar 10 dakikalık kör TTL'den geliyordu. Kör TTL, 'nasıl olsa düşer' cümlesinin kardeşi. Admin kaydettiğinde tag'i düşürmek, beklemeyi sıfıra indirir. revalidate saniye, tag ise kapı. İkisini birlikte kullanıyorum: tag anında, saniye ağ düşerse yedek.
Çerez okuyunca cache susar
Aynı sayfada cookies() varsa route dinamikleşir. fetch'e force-cache yazsan bile sayfa isteği tam statik HTML olarak ısınmaz. Tersine, çerez okumadan kullanıcıya özel URL'yi force-cache'lemek daha tehlikeli: yanıt paylaşılan cache'e yazılır, başka ziyaretçi ilk kişinin sepet özetini görebilir. Authorization başlığı çoğu sürümde cache'i ayırır veya kapatır; buna güvenmeden kullanıcı kimliğini URL'ye veya paylaşılan fetch'e koymuyorum.
import { cookies } from "next/headers";
export async function getInboxPreview() {
const jar = await cookies();
const session = jar.get("session")?.value;
if (!session) return [];
const response = await fetch(`${process.env.API_URL}/inbox/preview`, {
headers: { cookie: `session=${session}` },
cache: "no-store",
});
if (!response.ok) return [];
return response.json();
}
Inbox önizlemesi paylaşılan cache'e giremez. no-store burada utangaçlık değil, sözleşme. Vitrin fiyatı ise tam tersi: herkese aynı, tag ile düşer. İki fetch'i aynı helper'da 'varsayılan cache' ile sarmalamak, birinin kuralını diğerine bulaştırır. Helper'ı ikiye böldüm: publicGet ve privateGet. İmza, niyeti söylüyor.
Deduping cache değil
Aynı isteği generateMetadata ve page içinde iki kez yazmak, React'in request memoization'ı sayesinde bir fetch olabilir. Bu, data cache değil. Memoization istek bitince ölür. 'Zaten tek gitti' diye production cache'i kapalı sanmak, ertesi gün ısınmış fiyatı açıklamaz. Üç katman var: istek içi dedupe, data cache, router cache. Hangisinin ısırdığını söylemeden 'cache bozuk' demiyorum.
Router cache, client geçişlerinde sayfayı bir süre taze sayar. Tag düşürmek sunucu cache'ini temizler; açık sekme hâlâ eski ağacı tutuyor olabilir. refresh veya kısa stale time, o katmanı da ilgilendirir. Destek ticket'ında 'ctrl+r deyince düzeldi' varsa bakılacak yer çoğu zaman router cache. 'Herkeste eski' ise data cache.
URL anahtardır
fetch cache anahtarı URL ve seçili başlıklardır. Sayfalama parametresini body'de gönderip URL'yi sabit bırakmak, sayfa 2'nin yanıtını sayfa 1 diye tutabilir. GET ve query string, cache'in anlayacağı dil. POST'u 'çünkü cache'lenmesin' diye seçmek de ayrı bir hikâye: bazı CDN'ler ve ara katmanlar POST'u zaten tutmaz, sen de action veya route handler'a kayarsın.
Ortami karıştırmak ikinci klasikler. PRICING_URL staging'de localhost, production'da iç servis. Cache key URL olduğu için iki ortam birbirine sızmaz; ama aynı production URL'sine preview deployment'ından istek atmak, preview'ın ısınmış verisini production'a yazabilir. tag ve revalidate'i paylaşan bir data cache varsa, preview'ı ayrı cache store'a almak veya preview'da no-store kullanmak daha dürüst.
Yayına çıkmadan üç soru
- Bu yanıt herkese aynı mı, bir kişiye mi?
- Yazan biri kaydettiğinde okuyan ne kadar eski görebilir?
- Miss olursa kaynak ayakta mı, yoksa ısınmış kopya mı tercih?
Herkese aynı ve birkaç saniye eski olabilirse revalidate + tag. Kişiye özelse no-store veya request memoization. Miss'te kaynak düşerse ve eski vitrin kabul edilirse force-cache daha uzun yaşar; o zaman invalidation şart. Üç soruyu yazmadan 'default iyidir' deyip çıkmak, staging'in seni övmesi demek. Staging ısınmaz. Production ısınır.
Liste ve detay aynı tag ailesinde
Fiyatı offer:slug ile düşürmek detayı düzeltir. Anasayfa ızgarası catalog:home altında duruyorsa vitrin yine eski kartı basar. Tag'i tek kayda bağlamak, o kaydı gösteren her yüzeyi düşünmeden yarım iş. publishOffer içinde iki tag düşüyorum: kayıt ve onu listeleyen yüzeyler. Listeyi çok kaba bir catalog ile işaretlemek, her kayıtta tüm ızgarayı düşürür. Kaba, yanlış olmaktan iyidir; sessiz eski kart yanlış.
import { revalidateTag } from "next/cache";
export async function publishOffer(slug: string, featured: boolean) {
await writeOffer(slug);
revalidateTag(`offer:${slug}`, "max");
revalidateTag("catalog:home", "max");
if (featured) revalidateTag("catalog:featured", "max");
}
featured değilse featured tag'ine dokunmuyorum. Kör 'her şeyi düşür' de stampede üretir: yayın anında anasayfa, kategori ve arama aynı anda miss olur. Tag tasarımı, cache aside'taki prefix silmeye benzer. Hangi okuma hangi yazmada ölür, onu yazmadan force-cache açmıyorum.
Sürüm ezberi
14'ten 15'e geçince staging birden 'taze' oldu. İnsanlar bunu zafer sandı. Zafer değildi; varsayılan no-store'a kaymıştı, her vitrin isteği fiyat servisine gidiyordu. Fatura arttı, p95 uzadı, ticket bitti. Eski ezber 'fetch cache'ler'di, yeni ezber 'fetch cache'lemez'. İkisi de ikinci argümanı okumadan konuşuyor. next.config'teki cacheComponents veya experimental bayraklar da aynı kapıya dokunur. Sürüm notunu okumadan 'bizde fetch böyle' demek, bir major sonra tekrar ticket demek.
Kendi sarmalayıcımı yazdım: publicFetch ve userFetch. publicFetch revalidate ve tag ister, imza zorunlu. userFetch cache:'no-store' sabit, cookie veya Authorization alır. Ham fetch'i sayfada bırakmak, bir junior'ın force-cache'i kopyalamasına açık kapı. Sarmalayıcı niyeti isimde taşır. İsim, dokümantasyondan daha yakın durur.
Layout'tan fetch
Nav için layout'ta fetch etmek, o cache kuralını tüm çocuklara bağlar. cookies() layout'ta varsa bütün ağaç dinamikleşir. Statik blog yazıları birden her istekte ısınır. Nav oturuma göre değişiyorsa parçayı ayır: statik linkler layout'ta, kullanıcı menüsü ayrı bir dinamik çocukta, mümkünse Suspense içinde. Layout'u 'her şeyin veri kuyusu' yapmak, üçüncü yazının fiyat ticket'ını birinci yazının sınır hatasıyla birleştirir.
Yayına çıkmadan artık staging'i ılık bırakıyorum. Bir kez ısınıp ikinci isteğe bakmadan 'taze' demiyorum. İkinci istek, cache'in gerçek yüzü. Birinci istek her yerde taze durur.
revalidatePath ile tag
revalidatePath('/blog') kaba bir süpürge. Yazı güncellenince tüm blog ağacını düşürmek işe yarar, ısınma pahalıdır. Tag, süpürgenin ucunu inceltti. İkisini birden çağırmak, ince ucu anlamsızlaştırır. Path'i layout değişince, tag'i kayıt değişince kullanıyorum. 'Emin olmak için ikisi de' cümlesi, stampede'in kibar hali.
On demand revalidation'ı bir route handler'dan çağırmak, CMS webhook'u için doğru kapı. Form action içinden çağırmak, yazar kaydettiğinde doğru kapı. İkisinin aynı tag'i düşürmesi önemli; ikisinin aynı path'i körlemesine düşürmesi değil. Webhook'a secret koymayı unutmak, herkese cache temizlik düğmesi vermek. Bu, üçüncü yazının güvenlik dipnotu. Cache konuşurken kapıyı açık unutmak, fiyat ticket'ından daha pahalı.
no-store her yerde de değil
Ticket'tan sonra refleks 'her fetch no-store' oldu. Vitrin yine her istekte fiyat servisine gitti, p95 geri uzadı. Doğru düzeltme tag idi, küreği her yere vurmak değil. no-store kişiye özel ve anlık olması gereken yerde durur. Vitrin, kampanya anında tag ile düşen, normal günde 60 saniye eski olabilen yerdir. İkisini aynı helper'da birleştirmek, birinci haftanın hatasını tersinden tekrarlamak.
Ölçtüğüm üç sayı: hit oranı, miss'te kaynak süresi, revalidate sonrası ilk boya. Hit yoksa cache yok demektir, anahtar yanlıştır. Miss 400 ms ise 60 saniyelik revalidate kârdır. Revalidate sonrası hâlâ eskiyse tag yanlış yüzeye yapışmıştır. Üç sayı olmadan 'cache bozuk' demek, staging ile production'ı tekrar karıştırmak. Production ısınır. Isınmayı görmeden çıkan yazı, bu yazının başındaki ticket'tır.
Authorization başlığı
Paylaşılan bir katalog isteğine Authorization eklemek, cache'i ya kapatır ya kullanıcıya göre böler. Bölünmüş cache, her oturum için ısınmış kopya demek; bellek yer, hit düşer. Kapatılmış cache, herkese miss. Katalog herkese aynıysa başlığı koyma. Kullanıcıya özelse no-store veya ayrı URL. Başlığı 'nasıl olsa gider' diye eklemek, üçüncü kapıyı körlemesine kilitlemek.
Vary header'larını elle yönetmiyorum; Next'in fetch cache'i zaten belirli başlıkları anahtara katar. Kendi CDN katmanım varsa orada da cookie ve authorization'ı vary etmek gerekir. İki katman, iki anahtar. Birini unutunca 'ben no-store yazdım' dersin, kenardaki CDN ısınmış HTML basar. Ticket yine fiyat olur. Bu yüzden vitrin HTML'ini de kısa s-maxage ile, tag ile birlikte düşünüyorum. Data cache tek katman değil.
ISR alışkanlığını taşımak
Pages Router'daki getStaticProps + revalidate saniyesi, App Router'da fetch next.revalidate veya route segment config olarak duruyor. Alışkanlığı 'export const revalidate = 60' diye sayfanın tepesine yazmak, o sayfadaki her fetch'i aynı süreye bağlar. Bir fetch no-store isterse çatışma çıkar veya dinamikleşir. Segment config kör bir battaniyedir. Battaniyeyi vitrin listesine örtüyorum, kullanıcı paneline örtmüyorum.
generateStaticParams ile ısınmış slug'lar, ilk boyayı ucuzlatır. Parametrede olmayan bir slug ilk istekte dinamik ürer, sonra cache'lenebilir. Bunu 'tüm katalogu statik üret' diye abartmak, build'i saatlerce uzatır. Sıcak slug'ları üret, soğukları ilk istekte bırak. Cache, build'in yerine geçmez; build de her kaydı ısınmış sanmaz. Üçüncü yazı bu dengeyi fiyat ticket'ından öğrendi. Denge, varsayılan değil, yazılan ikinci argümandır.
Yanlış URL, yanlış hit
Trailing slash, locale öneki, gereksiz query — üçü de anahtarı böler. /offers/a ile /offers/a/ iki kopya ısınır. Locale'i path'e taşıyorsam fetch URL'sine de taşıyorum. Query'yi cache anahtarı yapmak istemiyorsam onu fetch'e koymuyorum; sayfada tutuyorum. Anahtar titreyince hit oranı yalan söyler. Yalan, yeni bir ticket'tır. Ticket'ın adı yine fiyattır, sebebi yine ikinci argümandır. Anahtarı yazmadan cache açan kişi, staging'in alkışına güveniyordur. Alkış ısınmaz.
Yorumlar
Yorumlar (0)
Yorumlar üyelere açık. Üye ol · Giriş yap