Bir blog yazısı ya da proje sayfası çoğu zaman günlerce, hatta aylarca değişmez. Ama klasik bir PHP sitesinde her ziyaret aynı işi baştan yapar: veritabanına bağlanır, birkaç sorgu çalıştırır, şablonları birleştirir ve sonunda bir önceki ziyaretçiye gönderdiği HTML'in aynısını üretir.
Tam sayfa önbelleği bu tekrarı ortadan kaldırır: sayfa ilk kez üretildiğinde çıktı bir dosyaya yazılır, sonraki isteklerde veritabanına hiç dokunulmadan doğrudan o dosya gönderilir. Bu sitede de aynı yöntemi kullanıyorum. Bu yazıda, ek bir kütüphane ya da sunucu yazılımı gerektirmeyen basit bir sayfa önbelleğini adım adım kuracağız ve yolda karşılaştığım tuzakları anlatacağım.
Paylaşımlı hosting performans yazısındaki OPcache, sıkıştırma ve önbellek başlıkları bu yöntemin tamamlayıcısıdır. OPcache PHP kodunun derlenmesini, sayfa önbelleği ise kodun hiç çalışmamasını sağlar.
Hangi sayfalar önbelleğe alınabilir?
Kural basit: her ziyaretçiye aynı görünen sayfalar. Ana sayfa, blog yazıları, proje detayları, hakkımda sayfası iyi adaylardır. Şunlar ise önbelleğe alınmamalıdır:
- Oturum açmış kullanıcıya özel sayfalar (yönetim paneli, hesap sayfası).
GETdışındaki istekler: form gönderimleri, API çağrıları.- Her yanıtta değişmesi gereken değer içeren sayfalar: CSRF jetonlu formlar gibi. Bunlara aşağıda ayrıca değineceğim.
- 200 dışında durum koduyla dönen yanıtlar: hata sayfasını önbelleğe almak, geçici bir sorunu kalıcı hâle getirir.
Önbellek anahtarı: hangi adres hangi dosya?
Her sayfa için benzersiz bir dosya adı gerekiyor. En kolayı, adresin MD5 özetini kullanmaktır. Ama adresi olduğu gibi kullanırsanız ?utm_source=twitter gibi takip parametreleri aynı sayfanın onlarca kopyasını üretir. Bu yüzden önce sorgu dizesini temizleyip sıralıyoruz:
private static function yol(): string
{
$adres = parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH) ?: '/';
parse_str($_SERVER['QUERY_STRING'] ?? '', $sorgu);
// Takip parametreleri önbelleği bölmesin
$sorgu = array_diff_key($sorgu, array_flip(['utm_source', 'utm_medium', 'utm_campaign', 'fbclid', 'gclid']));
ksort($sorgu); // ?a=1&b=2 ile ?b=2&a=1 aynı dosyaya düşsün
return self::KLASOR . '/' . md5($adres . '?' . http_build_query($sorgu)) . '.html';
}
Sınıfın tamamı
Mantık üç adımdan oluşuyor: istek uygunsa ve geçerli bir önbellek dosyası varsa onu gönder ve çık; yoksa çıktıyı tamponla, sayfa üretildikten sonra dosyaya yaz.
<?php
declare(strict_types=1);
final class SayfaOnbellegi
{
private const KLASOR = __DIR__ . '/../storage/cache/pages';
private const OMUR = 3600; // saniye
public static function uygunMu(): bool
{
return $_SERVER['REQUEST_METHOD'] === 'GET'
&& empty($_COOKIE[session_name()]); // oturum açık kullanıcıya özel içerik olabilir
}
public static function sun(): void
{
$yol = self::yol();
if (is_file($yol) && filemtime($yol) > time() - self::OMUR) {
header('X-Cache: HIT');
readfile($yol);
exit;
}
header('X-Cache: MISS');
ob_start();
}
public static function kaydet(): void
{
$html = ob_get_flush(); // tamponu ziyaretçiye gönder, kopyasını al
if ($html === false || $html === '' || http_response_code() !== 200) {
return;
}
if (!is_dir(self::KLASOR)) {
mkdir(self::KLASOR, 0750, true);
}
$hedef = self::yol();
$gecici = $hedef . '.' . bin2hex(random_bytes(4)) . '.tmp';
file_put_contents($gecici, $html, LOCK_EX);
rename($gecici, $hedef); // yarım yazılmış dosya asla sunulmaz
}
public static function temizle(): void
{
foreach (glob(self::KLASOR . '/*.html') ?: [] as $dosya) {
@unlink($dosya);
}
}
// yol() yukarıdaki örnekteki gibi
}<?php
require __DIR__ . '/../src/SayfaOnbellegi.php';
if (SayfaOnbellegi::uygunMu()) {
SayfaOnbellegi::sun(); // önbellekte varsa burada biter
register_shutdown_function([SayfaOnbellegi::class, 'kaydet']);
}
// ... normal yönlendirme, veritabanı ve şablonlarDikkat edilmesi gereken iki ayrıntı var. Birincisi rename(): dosyayı önce geçici bir adla yazıp sonra yeniden adlandırmak, aynı anda gelen başka bir isteğin yarım yazılmış bir dosyayı okumasını engeller. Aynı dosya sistemi içinde yeniden adlandırma tek adımda gerçekleşir. İkincisi X-Cache başlığı: ziyaretçiyi etkilemez ama bir sayfanın önbellekten gelip gelmediğini anlamayı çok kolaylaştırır.
Önbelleği ne zaman temizlemeli?
Önbelleğin en zor kısmı, onu doğru zamanda geçersiz kılmaktır. Bir yazıyı düzelttiğinizde eski hâlin bir saat daha görünmesi can sıkar. Küçük bir site için en güvenilir yöntem şaşırtıcı derecede basittir: içerik her kaydedildiğinde önbelleğin tamamını silmek.
// Yazı, proje ya da ayar kaydedildikten hemen sonra
$db->prepare('UPDATE posts SET title = ?, content = ? WHERE id = ?')->execute([$baslik, $icerik, $id]);
SayfaOnbellegi::temizle();
"Yalnız değişen sayfayı silsem daha verimli olmaz mı?" diye düşünebilirsiniz. Olur, ama bir yazının başlığı ana sayfada, blog listesinde, ilgili yazılar kutusunda ve site haritasında da görünür. Hangi sayfaların etkilendiğini tek tek izlemek hata yapmaya açıktır. Sayfalar zaten ilk ziyarette yeniden üretileceği için tamamını silmenin maliyeti çok düşüktür.
Tuzak 1: CSRF jetonları ve oturum
Sayfada bir iletişim formu ve formda CSRF jetonu varsa, önbelleğe alınan HTML ile birlikte jeton da dondurulur. Sonraki bütün ziyaretçiler aynı jetonu görür; jeton oturuma bağlıysa formları da çalışmaz. Çözüm seçenekleri:
- Form içeren sayfaları önbellekten muaf tutmak.
- Jetonu sayfa yüklendikten sonra küçük bir istekle (
fetch) almak. - Önbelleğe yazarken jetonun yerine bir yer tutucu koyup sunarken o ziyaretçiye özel jetonla değiştirmek.
Ayrıca önbelleğe uygun sayfalarda session_start() çağırmamaya dikkat edin. Oturum her ziyaretçiye çerez gönderir; çerezi olan ziyaretçi de yukarıdaki uygunMu() kontrolü yüzünden önbellekten yararlanamaz.
Tuzak 2: İçerik Güvenlik Politikası (CSP) nonce değeri
Satır içi betiklere CSP nonce değeriyle izin veriyorsanız, bu değerin her yanıtta farklı olması gerekir; amacı zaten tahmin edilemez olmaktır. Önbellekteki HTML ise eski nonce değerini taşır ve yeni yanıtın başlığıyla eşleşmez. Sonuç: betikleriniz sessizce engellenir. Çözüm, nonce değerini önbelleğe yer tutucu olarak yazıp her sunuşta yenisini üretmektir:
// Önbelleğe yazarken: gerçek nonce yerine yer tutucu
$html = str_replace($nonce, '__NONCE__', $html);
// Sunarken: her yanıta yeni bir nonce
$yeniNonce = base64_encode(random_bytes(16));
header("Content-Security-Policy: script-src 'self' 'nonce-$yeniNonce'");
echo str_replace('__NONCE__', $yeniNonce, file_get_contents($yol));
Ölçmeden inanmayın
Kazancı görmek için tarayıcı geliştirici araçlarına ya da tek satırlık bir curl komutuna bakmanız yeterli. Aynı adresi iki kez isteyin; ilk istek MISS, ikincisi HIT olmalı ve ilk bayt süresi belirgin şekilde düşmeli:
# Önbellek durumu
curl -sI https://ornek.com/blog | grep -i x-cache
# İlk bayt süresi (iki kez çalıştırıp karşılaştırın)
curl -s -o /dev/null -w "ilk bayt: %{time_starttransfer}s\n" https://ornek.com/blog
Fark, sayfanın ne kadar sorgu çalıştırdığına ve sunucunun yüküne göre değişir; ama veritabanına hiç gidilmeyen bir istek her zaman daha hızlıdır. Paylaşımlı hostingde bunun bir faydası daha var: sitenize aniden çok ziyaretçi geldiğinde veritabanı bağlantı sınırına takılma riskiniz azalır.
Özet
- Yalnız herkese aynı görünen
GETsayfalarını ve yalnız 200 yanıtlarını önbelleğe alın. - Önbellek anahtarında takip parametrelerini ayıklayın, sorgu dizesini sıralayın.
- Dosyayı geçici adla yazıp
rename()ile yerine koyun. - İçerik her değiştiğinde önbelleğin tamamını temizleyin.
- CSRF jetonlarını, oturumu ve CSP nonce değerini unutmayın.
X-Cachebaşlığı vecurlile ölçün.

Yorum yazın
Üyelik gerekmez. E-posta adresiniz sitede görünmez ve kimseyle paylaşılmaz.