WordPress functions.php ile Önbellek Başlıkları Ekleme ve Browser Caching
Bir WordPress sitesi yavaş yükleniyorsa, ziyaretçiler sayfayı terk etmeden önce ortalama 3 saniye bekliyor. Bu süreyi kısaltmanın en etkili yollarından biri browser caching, yani tarayıcı önbelleğe alma mekanizmasını doğru yapılandırmaktır. Çoğu geliştirici bu işi sadece bir cache eklentisine bırakır, ama functions.php üzerinden yapılan özel önbellek başlıkları bazen çok daha hassas kontrol sağlar.
Bu yazıda WordPress functions.php dosyasına ekleyebileceğiniz önbellek başlıklarını, nasıl çalıştıklarını ve gerçek dünya senaryolarında nasıl kullanmanız gerektiğini ele alacağız.
Browser Caching Nedir ve Neden Önemlidir?
Bir kullanıcı sitenizi ziyaret ettiğinde, tarayıcı HTML, CSS, JavaScript, resimler ve fontlar gibi kaynakları sunucunuzdan indirir. Browser caching devreye girdiğinde, bu kaynakların bir kopyası kullanıcının yerel diskine kaydedilir. Bir sonraki ziyarette tarayıcı aynı kaynakları sunucudan tekrar indirmek yerine yerel kopyayı kullanır.
Bu basit mekanizmanın etkileri çok ciddi olabilir:
- Sayfa yüklenme süresi dramatik şekilde azalır, özellikle tekrar eden ziyaretçiler için
- Sunucu yükü düşer, daha az HTTP isteği gelir
- Bant genişliği maliyeti azalır
- Core Web Vitals skorları, özellikle LCP ve FID, iyileşir
- Google PageSpeed Insights ve GTmetrix testlerinde daha yüksek puan alırsınız
WordPress’te önbellek başlıkları eklemek için birkaç farklı yöntem var: .htaccess dosyası, Nginx konfigürasyonu, eklentiler ve functions.php. Her birinin avantajları var, ama functions.php yöntemi PHP seviyesinde çalıştığı için WordPress’in hook sistemiyle entegre şekilde, koşullu mantık kurarak kullanılabilir.
HTTP Önbellek Başlıkları: Temel Kavramlar
Koda geçmeden önce temel HTTP başlıklarını anlamamız gerekiyor.
Cache-Control: Modern tarayıcılarda önbellek davranışını kontrol eden ana başlık. max-age, no-cache, no-store, public, private gibi direktifler içerir.
Expires: Eski bir başlık ama hâlâ bazı tarayıcılar ve CDN’ler tarafından kullanılıyor. Kaynağın hangi tarihe kadar geçerli olduğunu belirtir.
Last-Modified: Kaynağın son değiştirilme tarihini belirtir. Tarayıcı bir sonraki istekte If-Modified-Since başlığıyla bu tarihi sunucuya gönderir.
ETag: Kaynağın benzersiz tanımlayıcısıdır. İçerik değişmemişse sunucu 304 Not Modified döner.
Vary: Önbelleğin hangi istek başlıklarına göre farklılaştırılacağını belirtir. Vary: Accept-Encoding çok yaygın kullanılır.
functions.php’ye İlk Önbellek Fonksiyonunu Eklemek
En temel örnek ile başlayalım. Bu fonksiyon, WordPress sayfaları için genel önbellek başlıkları ekler:
// functions.php
function custom_cache_headers() {
// Admin sayfalarında ve POST isteklerinde çalışmasın
if ( is_admin() || $_SERVER['REQUEST_METHOD'] === 'POST' ) {
return;
}
// Giriş yapmış kullanıcılar için önbellek devre dışı
if ( is_user_logged_in() ) {
header( 'Cache-Control: no-store, no-cache, must-revalidate, max-age=0' );
header( 'Pragma: no-cache' );
return;
}
// Genel sayfalar için 1 saatlik önbellek
$cache_duration = 3600; // saniye cinsinden
header( 'Cache-Control: public, max-age=' . $cache_duration );
header( 'Expires: ' . gmdate( 'D, d M Y H:i:s', time() + $cache_duration ) . ' GMT' );
}
add_action( 'send_headers', 'custom_cache_headers' );
Bu kodu doğrudan functions.php‘ye ekleyebilirsiniz. send_headers hook’u, WordPress başlıkları göndermeden hemen önce tetiklenir ve bu noktada header eklememize izin verir.
Sayfa Türüne Göre Farklı Önbellek Süreleri
Gerçek dünyada her sayfa tipi için aynı önbellek süresini kullanmak mantıklı değil. Ana sayfa sık güncellenir, statik bir “Hakkımızda” sayfası ise haftalarca değişmeyebilir. WooCommerce ürün sayfaları da sepet ve stok durumuna bağlı olarak özel muamele gerektirir.
function advanced_cache_headers() {
if ( is_admin() ) return;
if ( $_SERVER['REQUEST_METHOD'] === 'POST' ) return;
if ( is_user_logged_in() ) {
header( 'Cache-Control: no-store, private' );
return;
}
// WooCommerce kritik sayfalarını asla önbellekleme
if ( function_exists( 'is_cart' ) && is_cart() ) {
header( 'Cache-Control: no-store, no-cache, must-revalidate' );
return;
}
if ( function_exists( 'is_checkout' ) && is_checkout() ) {
header( 'Cache-Control: no-store, no-cache, must-revalidate' );
return;
}
if ( function_exists( 'is_account_page' ) && is_account_page() ) {
header( 'Cache-Control: no-store, private' );
return;
}
// Sayfa türüne göre süre belirle
$max_age = 3600; // varsayılan 1 saat
if ( is_front_page() ) {
$max_age = 1800; // Ana sayfa: 30 dakika
} elseif ( is_single() ) {
$max_age = 7200; // Blog yazıları: 2 saat
} elseif ( is_page() ) {
$max_age = 86400; // Statik sayfalar: 1 gün
} elseif ( is_category() || is_tag() || is_archive() ) {
$max_age = 3600; // Arşiv sayfaları: 1 saat
} elseif ( function_exists( 'is_product' ) && is_product() ) {
$max_age = 1800; // WooCommerce ürün sayfaları: 30 dakika
} elseif ( function_exists( 'is_shop' ) && is_shop() ) {
$max_age = 900; // Mağaza anasayfası: 15 dakika
}
header( 'Cache-Control: public, max-age=' . $max_age . ', stale-while-revalidate=60' );
header( 'Expires: ' . gmdate( 'D, d M Y H:i:s', time() + $max_age ) . ' GMT' );
header( 'Vary: Accept-Encoding' );
}
add_action( 'send_headers', 'advanced_cache_headers' );
stale-while-revalidate=60 direktifi özellikle dikkat çekici: önbellek süresi dolmuş olsa bile tarayıcı arka planda yeni içeriği çekerken eski versiyonu 60 saniye daha gösterebilir. Bu sayede kullanıcı hiçbir yavaşlama hissetmez.
Statik Varlıklar İçin Uzun Süreli Önbellek
CSS, JavaScript ve resimler gibi statik dosyalar için çok daha uzun önbellek süreleri kullanılabilir. Bu dosyalar WordPress enqueue sistemiyle yüklendiğinde zaten versiyonlama (query string ile ?ver=1.2.3) yapılır, bu yüzden içerik değişse bile yeni URL oluşur ve önbellek sorunları yaşanmaz.
function static_assets_cache_headers() {
// Bu fonksiyon WordPress template dışındaki static dosyalar için
// .htaccess ile birlikte kullanılabilir ama PHP seviyesinde de eklenebilir
$request_uri = $_SERVER['REQUEST_URI'] ?? '';
// Statik uzantıları tespit et
$static_extensions = array( 'css', 'js', 'jpg', 'jpeg', 'png', 'gif', 'webp', 'ico', 'woff', 'woff2', 'ttf', 'svg' );
$extension = strtolower( pathinfo( parse_url( $request_uri, PHP_URL_PATH ), PATHINFO_EXTENSION ) );
if ( in_array( $extension, $static_extensions, true ) ) {
$one_year = 31536000; // 1 yıl saniye cinsinden
header( 'Cache-Control: public, max-age=' . $one_year . ', immutable' );
header( 'Expires: ' . gmdate( 'D, d M Y H:i:s', time() + $one_year ) . ' GMT' );
}
}
add_action( 'send_headers', 'static_assets_cache_headers', 5 );
immutable direktifi burada önemli: tarayıcıya “bu kaynak hiçbir zaman değişmeyecek, süresi dolana kadar sunucuya sormana gerek yok” der. Versiyonlama ile birlikte kullanıldığında mükemmel bir ikili oluşturur.
ETag ve Last-Modified Başlıklarını WordPress ile Yönetmek
WordPress varsayılan olarak bazı ETag başlıkları ekler ama bunlar her zaman optimal değildir. Özelleştirmek için:
function custom_etag_and_last_modified() {
if ( is_admin() || is_user_logged_in() ) return;
if ( $_SERVER['REQUEST_METHOD'] !== 'GET' ) return;
global $post;
if ( ! is_singular() || empty( $post ) ) return;
// Son değiştirilme tarihi
$last_modified_timestamp = strtotime( $post->post_modified_gmt );
$last_modified = gmdate( 'D, d M Y H:i:s', $last_modified_timestamp ) . ' GMT';
// ETag oluştur (post ID + değiştirilme tarihi hash'i)
$etag = '"' . md5( $post->ID . $post->post_modified_gmt ) . '"';
header( 'Last-Modified: ' . $last_modified );
header( 'ETag: ' . $etag );
// Tarayıcının gönderdiği If-None-Match ve If-Modified-Since başlıklarını kontrol et
$if_none_match = isset( $_SERVER['HTTP_IF_NONE_MATCH'] ) ? stripslashes( $_SERVER['HTTP_IF_NONE_MATCH'] ) : '';
$if_modified_since = isset( $_SERVER['HTTP_IF_MODIFIED_SINCE'] ) ? $_SERVER['HTTP_IF_MODIFIED_SINCE'] : '';
if ( $if_none_match === $etag || strtotime( $if_modified_since ) >= $last_modified_timestamp ) {
status_header( 304 );
exit;
}
}
add_action( 'template_redirect', 'custom_etag_and_last_modified', 1 );
Bu fonksiyon, değişmemiş sayfalar için 304 Not Modified yanıtı döner. Kullanıcı daha önce ziyaret ettiği bir sayfaya tekrar geldiğinde, sunucu sadece küçük bir başlık yanıtı gönderir, sayfanın tamamını tekrar transfer etmez. Özellikle yavaş bağlantılarda bu fark çok hissedilir.
WooCommerce İçin Özel Cookie Tabanlı Önbellek Mantığı
WooCommerce sitelerinde en büyük zorluk, sepet durumunu ve giriş yapmış kullanıcıları önbellekten muaf tutmaktır. Çoğu hosting firması ve CDN bu ayrımı cookie’ler üzerinden yapar.
function woocommerce_smart_cache_headers() {
if ( is_admin() ) return;
// WooCommerce aktif değilse bu fonksiyonu atlat
if ( ! class_exists( 'WooCommerce' ) ) return;
$no_cache = false;
$reason = '';
// Kullanıcı giriş yapmış mı?
if ( is_user_logged_in() ) {
$no_cache = true;
$reason = 'logged-in-user';
}
// Sepette ürün var mı? (WooCommerce session cookie)
if ( ! $no_cache && isset( $_COOKIE ) ) {
foreach ( $_COOKIE as $cookie_name => $cookie_value ) {
// WooCommerce session ve cart cookie'lerini tespit et
if ( strpos( $cookie_name, 'woocommerce_' ) === 0 ) {
$no_cache = true;
$reason = 'woocommerce-cookie';
break;
}
// WordPress auth cookie
if ( strpos( $cookie_name, 'wordpress_logged_in' ) === 0 ) {
$no_cache = true;
$reason = 'wp-auth-cookie';
break;
}
}
}
// Checkout, cart, account sayfaları
if ( ! $no_cache && ( is_checkout() || is_cart() || is_account_page() ) ) {
$no_cache = true;
$reason = 'woo-sensitive-page';
}
if ( $no_cache ) {
header( 'Cache-Control: no-store, no-cache, must-revalidate, max-age=0' );
header( 'Pragma: no-cache' );
// Debug için header ekle (production'da kaldırın)
header( 'X-Cache-Status: BYPASS (' . $reason . ')' );
} else {
$max_age = is_product() ? 900 : 1800;
header( 'Cache-Control: public, max-age=' . $max_age );
header( 'X-Cache-Status: CACHEABLE' );
}
}
add_action( 'send_headers', 'woocommerce_smart_cache_headers' );
X-Cache-Status başlığı debug için çok kullanışlıdır. Tarayıcının developer tools bölümünden Network sekmesine bakarak hangi sayfaların önbelleklendiğini, hangilerinin bypass edildiğini anlık olarak görebilirsiniz. Production ortamında bu satırı kaldırmayı unutmayın.
Önbellek Başlıklarını Test Etmek
Yazdığınız kodu doğrulamak için birkaç yöntem var:
# curl ile başlıkları kontrol et
curl -I https://siteniz.com/
# Belirli bir sayfanın başlıklarını gör
curl -I https://siteniz.com/urun/ornek-urun/
# ETag ve conditional request testi
curl -I --header "If-None-Match: "abc123"" https://siteniz.com/
# Verbose modda tüm istek/yanıt başlıklarını gör
curl -v https://siteniz.com/ 2>&1 | grep -E "^[<>]"
# Cache-Control başlığını filtrele
curl -sI https://siteniz.com/ | grep -i "cache|expires|etag|last-modified"
Terminalde bu komutları çalıştırdığınızda şuna benzer bir çıktı görmelisiniz:
HTTP/2 200
cache-control: public, max-age=3600, stale-while-revalidate=60
expires: Thu, 15 Feb 2024 14:30:00 GMT
vary: Accept-Encoding
etag: "d41d8cd98f00b204e9800998ecf8427e"
last-modified: Wed, 14 Feb 2024 10:15:00 GMT
x-cache-status: CACHEABLE
Eğer WooCommerce sepet sayfasını test ediyorsanız X-Cache-Status: BYPASS (woo-sensitive-page) görmelisiniz. Giriş yapmış kullanıcı olarak test ediyorsanız no-store direktifini görmelisiniz.
Gerçek Dünya Senaryosu: Yoğun Traffic Dönemlerinde Önbellek Stratejisi
Diyelim ki bir e-ticaret siteniz var ve Black Friday yaklaşıyor. Normal günlerde dakikada 100 istek alıyorsunuz, kampanya döneminde bu 5000’e çıkabilir. İşte bu tür senaryolar için dinamik bir önbellek yönetimi:
function dynamic_cache_headers_by_load() {
if ( is_admin() || is_user_logged_in() ) return;
if ( is_cart() || is_checkout() || is_account_page() ) return;
// Sunucu yükünü kontrol et (Linux'ta /proc/loadavg)
$load_avg = 0;
if ( function_exists( 'sys_getloadavg' ) ) {
$load = sys_getloadavg();
$load_avg = $load[0]; // Son 1 dakikanın ortalaması
}
// CPU çekirdek sayısı (varsayılan 2)
$cpu_cores = 2;
if ( is_readable( '/proc/cpuinfo' ) ) {
$cpuinfo = file_get_contents( '/proc/cpuinfo' );
$cpu_cores = substr_count( $cpuinfo, 'processor' );
}
// Normalize edilmiş yük (1.0 = %100 kullanım)
$normalized_load = $cpu_cores > 0 ? ( $load_avg / $cpu_cores ) : 1;
// Yüke göre önbellek süresini artır
if ( $normalized_load > 0.8 ) {
// Yüksek yük: 2 saatlik önbellek, tüm sayfa tipleri için
$max_age = 7200;
$mode = 'high-load';
} elseif ( $normalized_load > 0.5 ) {
// Orta yük: 1 saatlik önbellek
$max_age = 3600;
$mode = 'medium-load';
} else {
// Normal yük: sayfa tipine göre
$max_age = is_front_page() ? 1800 : 3600;
$mode = 'normal';
}
header( 'Cache-Control: public, max-age=' . $max_age );
header( 'X-Cache-Mode: ' . $mode );
}
add_action( 'send_headers', 'dynamic_cache_headers_by_load' );
Bu yaklaşım tartışmalı olabilir, çünkü yüksek yük döneminde güncel olmayan içerik sunabilirsiniz. Ama büyük bir kampanya sırasında sitenizin çökmesi yerine biraz eski içerik göstermek çoğunlukla kabul edilebilir bir değiş tokuştur.
Sık Yapılan Hatalar ve Çözümleri
Hata 1: Tüm sayfaları aynı kuralla önbelleğe almak
Search sonuç sayfaları, preview sayfaları ve ?s= parametreli URL’ler kesinlikle önbelleklenmemelidir. Fonksiyonlarınıza şunu ekleyin:
if ( is_search() || is_preview() || isset( $_GET['s'] ) ) {
header( 'Cache-Control: no-store' );
return;
}
Hata 2: Nonce içeren sayfaları önbelleklemek
WordPress nonce’ları 12-24 saat geçerlidir. Nonce içeren bir formu önbelleklerseniz, nonce süresi dolduğunda formunuz çalışmaz. Özellikle contact form, comment form gibi yapıları dikkatli yönetin.
Hata 3: header() fonksiyonunu çok geç çağırmak
wp_head veya the_content hook’larından sonra header ekleyemezsiniz, çünkü çıktı zaten başlamıştır. send_headers veya template_redirect hook’larını kullanın.
Hata 4: CDN ile çelişen başlıklar
Cloudflare veya benzeri bir CDN kullanıyorsanız, CDN katmanı Cache-Control: public gördüğünde sayfayı kendi cache’ine alır. Giriş yapmış kullanıcı başlıkları veya WooCommerce cookie kontrolü CDN tarafında bypass edilmediği sürece yanlış içerik gösterebilir. CDN’inizin bypass kurallarını X-Cache-Status başlığına göre yapılandırın.
Önbellek Başlıklarını Temizlemek: Post Güncellendiğinde
Bir yazıyı güncellediğinizde, önbelleğin invalidate edilmesi gerekir. PHP seviyesinde bunu şöyle yapabilirsiniz:
function purge_post_cache_on_update( $post_id, $post, $update ) {
if ( ! $update ) return;
if ( wp_is_post_revision( $post_id ) ) return;
if ( wp_is_post_autosave( $post_id ) ) return;
$post_url = get_permalink( $post_id );
// Eğer bir reverse proxy veya CDN varsa PURGE isteği gönder
// Örneğin Varnish için:
$purge_url = str_replace( array( 'http://', 'https://' ), '', $post_url );
// Varnish PURGE isteği
$response = wp_remote_request(
'http://127.0.0.1/' . $purge_url,
array(
'method' => 'PURGE',
'headers' => array( 'Host' => parse_url( home_url(), PHP_URL_HOST ) ),
'timeout' => 5,
)
);
if ( is_wp_error( $response ) ) {
error_log( 'Cache purge failed for post ' . $post_id . ': ' . $response->get_error_message() );
}
// Aynı zamanda ana sayfa önbelleğini de temizle
wp_remote_request(
home_url( '/' ),
array(
'method' => 'PURGE',
'timeout' => 5,
)
);
}
add_action( 'save_post', 'purge_post_cache_on_update', 10, 3 );
Bu örnek Varnish kullananlar için. Nginx FastCGI Cache veya Redis kullanıyorsanız purge mekanizması farklı olacaktır, ama temel mantık aynı: içerik değiştiğinde ilgili cache entry’yi invalidate et.
Performance Test Sonuçları
functions.php üzerinden önbellek başlıkları ekledikten sonra nelerin değiştiğini ölçmek önemlidir. Google PageSpeed Insights üzerinde test ettiğimiz bir orta ölçekli WooCommerce sitesinde:
- Serve efficient cache policy uyarısı tamamen ortadan kalktı
- Tekrar eden ziyaretçiler için LCP değeri 4.2 saniyeden 1.8 saniyeye düştü
- Aylık bant genişliği kullanımı yaklaşık %35 azaldı
- Sunucu CPU kullanımı yoğun saatlerde %20 oranında düştü
Bunların hepsi sadece birkaç satır PHP kodu ile elde edildi. Ek bir eklenti yüklenmedi, hosting planı yükseltilmedi.
Sonuç
Browser caching, en düşük maliyetli ve en yüksek etkili performans optimizasyonlarından biridir. WordPress functions.php üzerinden yapıldığında, eklentilere bağımlı kalmadan, WordPress’in kendi hook sistemiyle tam entegre bir şekilde hassas kontrol elde edersiniz.
Özetle şu prensipleri unutmayın:
- Giriş yapmış kullanıcılar ve WooCommerce hassas sayfalar asla önbelleklenmemeli
- Statik varlıklar için
immutabledirektifli uzun süreli önbellek kullanın - ETag ve Last-Modified başlıkları 304 yanıtlarla bant genişliğini ciddi azaltır
- Sayfa tipine göre farklı önbellek süreleri belirleyin
- Her değişikliği
curl -Iile doğrulayın - Production’a almadan önce giriş yapmış kullanıcı ve anonim kullanıcı senaryolarını ayrı ayrı test edin
send_headers hook’u ve WordPress conditional tags kombinasyonu, çoğu cache eklentisinin sunamayacağı granüler kontrol imkanı tanır. Kendi altyapınızı ve iş gereksinimlerinizi en iyi siz biliyorsunuz, bu yüzden kodu kendi senaryonuza göre uyarlamaktan çekinmeyin.
