Performans Optimizasyonu: WordPress Eklenti Hız Ayarları

Üretim ortamında WordPress eklenti performansıyla ciddi anlamda boğuşmaya başlayana kadar insan bu konuyu gerçekten önemsemiyor. Bir e-ticaret sitesinde 40+ aktif eklenti, sayfa yüklenme süresi 8 saniye, müşteri şikayetleri hat safhada. İşte o noktada “eklenti yönetimi” kavramı soyut bir şeyden somut bir krize dönüşüyor. Bu yazıda hem eklenti geliştirme tarafında hem de yönetim tarafında uygulayabileceğiniz gerçek performans tekniklerini paylaşacağım.

Eklenti Performans Sorunlarının Anatomisi

Çoğu WordPress eklentisi performans sorunu aslında birkaç temel kategoriye giriyor: gereksiz veritabanı sorguları, önbelleğe alınmamış işlemler, her sayfaya yüklenen ağır scriptler ve senkron harici API çağrıları. Bu dördünü çözebilirseniz, eklenti kaynaklı sorunların büyük çoğunluğunu elimine etmiş olursunuz.

İlk adım her zaman ölçmek. Tahmine dayalı optimizasyon yapmak yerine neyin yavaş olduğunu tespit etmek gerekiyor.

Query Monitor ile Teşhis

Query Monitor eklentisi production’a kurmayın ama staging veya development ortamınızda bu araç altın değerinde. Her sayfa yüklemesinde çalışan sorguları, hook zamanlamalarını ve HTTP isteklerini görebiliyorsunuz.

// wp-config.php içinde geliştirme ortamında aktif edin
define('SAVEQUERIES', true);
define('WP_DEBUG', true);
define('WP_DEBUG_LOG', true);
define('WP_DEBUG_DISPLAY', false);

// functions.php veya plugin dosyanızda sorgu analizini loglayın
add_action('shutdown', function() {
    if (defined('SAVEQUERIES') && SAVEQUERIES && current_user_can('administrator')) {
        global $wpdb;
        $slow_queries = array_filter($wpdb->queries, function($query) {
            return $query[1] > 0.05; // 50ms üzeri sorgular
        });
        
        if (!empty($slow_queries)) {
            error_log('YAVAS SORGULAR: ' . print_r($slow_queries, true));
        }
    }
});

Bu kodu development ortamında çalıştırıp log dosyanıza baktığınızda hangi sorguların 50ms üzerinde kaldığını görürsünüz. Üretimde aynı bilgiyi elde etmek için APM araçlarına (New Relic, Datadog) veya en azından bir slow query log konfigürasyonuna ihtiyacınız var.

Veritabanı Sorgu Optimizasyonu

Eklenti geliştiriyorsanız veya mevcut bir eklentinin koduna müdahale edebiliyorsanız, sorgu optimizasyonu en yüksek etkiyi sağlayan alan.

Tekrarlayan Sorguları Transient API ile Önbelleğe Alın

Pek çok eklenti aynı veriyi her sayfa yüklemesinde veritabanından çekiyor. Özellikle sayaçlar, istatistikler veya nadiren değişen veriler için bu gereksiz bir yük oluşturuyor.

function get_product_stats_optimized() {
    $cache_key = 'product_stats_v2';
    $stats = get_transient($cache_key);
    
    if (false === $stats) {
        global $wpdb;
        
        // Ağır sorgu - ama sadece cache expire olduğunda çalışacak
        $stats = $wpdb->get_results("
            SELECT 
                p.post_status,
                COUNT(*) as total,
                AVG(pm.meta_value) as avg_price
            FROM {$wpdb->posts} p
            LEFT JOIN {$wpdb->postmeta} pm 
                ON p.ID = pm.post_id 
                AND pm.meta_key = '_price'
            WHERE p.post_type = 'product'
            GROUP BY p.post_status
        ", ARRAY_A);
        
        // 1 saatlik önbellek - woo_product_stats hook'uyla temizlenebilir
        set_transient($cache_key, $stats, HOUR_IN_SECONDS);
    }
    
    return $stats;
}

// Ürün güncellendiğinde cache'i temizle
add_action('save_post_product', function() {
    delete_transient('product_stats_v2');
});

Burada önemli bir nokta: transient key’lerine versiyon eklemek. product_stats yerine product_stats_v2 kullanmak, gelecekte veri yapısı değiştiğinde eski cache’i geçersiz kılmak için uygulanan yaygın bir pattern.

N+1 Sorgu Problemini Çözün

WordPress geliştiricilerinin en sık düştüğü tuzak N+1 sorgu problemi. 100 ürün için 100 ayrı get_post_meta() çağrısı yapmak yerine toplu veri çekmeyi tercih edin.

// YANLIŞ - N+1 problemi
function get_products_wrong($product_ids) {
    $results = [];
    foreach ($product_ids as $id) {
        $results[$id] = get_post_meta($id, '_price', true); // Her döngüde 1 sorgu!
    }
    return $results;
}

// DOĞRU - Tek sorguda toplu çekiş
function get_products_optimized($product_ids) {
    global $wpdb;
    
    if (empty($product_ids)) {
        return [];
    }
    
    $ids_placeholder = implode(',', array_map('intval', $product_ids));
    
    $results = $wpdb->get_results("
        SELECT post_id, meta_value 
        FROM {$wpdb->postmeta}
        WHERE post_id IN ({$ids_placeholder})
        AND meta_key = '_price'
    ", ARRAY_A);
    
    $prices = [];
    foreach ($results as $row) {
        $prices[$row['post_id']] = $row['meta_value'];
    }
    
    return $prices;
}

100 ürünlük bir listede bu fark 100 sorgudan 1 sorguya inmek anlamına geliyor. Sayfa yüklenme süresine etkisi milisaniyelerden saniyeye uzanan bir aralıkta olabiliyor.

Script ve Style Yönetimi

Eklentilerin en sinir bozucu özelliklerinden biri, ihtiyaç duyulmayan sayfalarda bile CSS ve JavaScript dosyalarını yüklemeleri. İletişim formu eklentisinin scriptleri ana sayfada ne işi var?

Koşullu Script Yükleme

class My_Plugin_Assets {
    
    public function __construct() {
        add_action('wp_enqueue_scripts', [$this, 'conditionally_load_assets']);
    }
    
    public function conditionally_load_assets() {
        // Sadece iletişim sayfasında yükle
        if (!is_page('iletisim') && !$this->page_has_shortcode('my_contact_form')) {
            return;
        }
        
        wp_enqueue_style(
            'my-plugin-style',
            MY_PLUGIN_URL . 'assets/css/main.min.css',
            [],
            MY_PLUGIN_VERSION
        );
        
        wp_enqueue_script(
            'my-plugin-script',
            MY_PLUGIN_URL . 'assets/js/main.min.js',
            ['jquery'],
            MY_PLUGIN_VERSION,
            true // Footer'da yükle
        );
        
        // Inline data - harici dosya yerine
        wp_localize_script('my-plugin-script', 'myPluginData', [
            'ajaxUrl' => admin_url('admin-ajax.php'),
            'nonce'   => wp_create_nonce('my_plugin_nonce'),
            'i18n'    => [
                'loading' => __('Yükleniyor...', 'my-plugin'),
                'error'   => __('Bir hata oluştu.', 'my-plugin'),
            ]
        ]);
    }
    
    private function page_has_shortcode($shortcode) {
        global $post;
        return is_a($post, 'WP_Post') && has_shortcode($post->post_content, $shortcode);
    }
}

is_page(), is_singular(), is_archive() gibi conditional tag’leri kullanmayı alışkanlık haline getirin. Bunu yapan eklentilerle yapmayanlar arasındaki fark, 15 aktif eklentili bir sitede kolayca 500KB+ gereksiz kaynak yükü olarak ortaya çıkıyor.

Admin Scriptlerini de Filtreleyin

add_action('admin_enqueue_scripts', function($hook) {
    // Sadece kendi plugin sayfamızda yükle
    if ('toplevel_page_my-plugin' !== $hook) {
        return;
    }
    
    wp_enqueue_script(
        'my-plugin-admin',
        MY_PLUGIN_URL . 'assets/js/admin.min.js',
        ['wp-components', 'wp-data'],
        MY_PLUGIN_VERSION,
        true
    );
});

Asenkron İşlemler ve Background Jobs

Bazı işlemler kullanıcıyı bekletmemeli. E-posta gönderimi, rapor oluşturma, harici API senkronizasyonu bunların başında geliyor.

Action Scheduler ile Kuyruk Yönetimi

WooCommerce’in içinde gelen Action Scheduler kütüphanesi WordPress ekosisteminin en güvenilir background job çözümlerinden biri. Bağımsız olarak da kullanabilirsiniz.

// Ağır işlemi kuyruğa al - kullanıcıyı beklettirme
function queue_report_generation($user_id, $date_range) {
    if (function_exists('as_schedule_single_action')) {
        as_schedule_single_action(
            time() + 5, // 5 saniye sonra
            'my_plugin_generate_report',
            ['user_id' => $user_id, 'range' => $date_range],
            'my-plugin-reports'
        );
        
        return ['status' => 'queued', 'message' => 'Rapor hazırlanıyor, bildirim alacaksınız.'];
    }
    
    // Fallback: WP Cron
    wp_schedule_single_event(
        time() + 10,
        'my_plugin_generate_report_cron',
        [$user_id, $date_range]
    );
}

// İşlemi gerçekleştiren handler
add_action('my_plugin_generate_report', function($args) {
    $user_id = $args['user_id'];
    $range   = $args['range'];
    
    // Ağır rapor oluşturma işlemi burada
    $report_data = generate_heavy_report($user_id, $range);
    
    // Sonucu kaydet
    update_user_meta($user_id, 'pending_report_' . $range, $report_data);
    
    // Kullanıcıya bildirim gönder
    wp_mail(
        get_userdata($user_id)->user_email,
        'Raporunuz Hazır',
        'Talep ettiğiniz rapor hazır.'
    );
});

Object Cache Entegrasyonu

WordPress’in varsayılan object cache’i sadece tek bir sayfa yüklemesi boyunca geçerli. Redis veya Memcached entegrasyonuyla bu cache kalıcı hale geliyor ve eklentilerinizi bu mekanizmayı göz önünde bulundurarak yazmanız büyük fark yaratıyor.

function get_expensive_calculation($param) {
    // Hem transient hem de object cache destekli hibrit yaklaşım
    $cache_group = 'my_plugin_calcs';
    $cache_key   = 'calc_' . md5(serialize($param));
    
    // Önce object cache'e bak (Redis/Memcached varsa buradan gelir)
    $result = wp_cache_get($cache_key, $cache_group);
    
    if (false !== $result) {
        return $result;
    }
    
    // Object cache'de yoksa hesapla
    $result = perform_expensive_calculation($param);
    
    // Object cache'e yaz (persistent cache varsa kalıcı, yoksa sayfa ömrü kadar)
    wp_cache_set($cache_key, $result, $cache_group, 3600);
    
    // Güvenlik için transient'a da yaz
    set_transient($cache_key, $result, 3600);
    
    return $result;
}

// Cache'i grup bazında temizleme
function clear_plugin_cache() {
    wp_cache_flush_group('my_plugin_calcs'); // Redis 6.2+ ve bazı object cache eklentileri
    
    // Alternatif: versiyon tabanlı cache geçersizleştirme
    wp_cache_set('my_plugin_cache_version', time(), 'my_plugin', 0);
}

Redis ile WordPress Konfigürasyonu

Eğer sunucu tarafında Redis kuruluysa, wp-config.php üzerinde birkaç ayarla nesne önbellekleme aktif hale getirilebilir:

# Redis'i kurun (Ubuntu/Debian)
sudo apt install redis-server php-redis

# Redis durumunu kontrol edin
redis-cli ping
# PONG dönmeli

# WordPress için object-cache.php dosyasını yerleştirin
# (redis-cache veya w3-total-cache eklentisi bunu otomatik yapar)
wp plugin install redis-cache --activate
wp redis enable
wp redis status
// wp-config.php içinde Redis bağlantı ayarları
define('WP_REDIS_HOST', '127.0.0.1');
define('WP_REDIS_PORT', 6379);
define('WP_REDIS_TIMEOUT', 1);
define('WP_REDIS_READ_TIMEOUT', 1);
define('WP_REDIS_DATABASE', 0);
define('WP_REDIS_PREFIX', 'mysite_'); // Çoklu site kurulumlarında şart
define('WP_REDIS_MAXTTL', 86400); // Maksimum 24 saat

Hook Optimizasyonu: Gereksiz Filter ve Action’lardan Kaçının

Eklenti geliştirirken hook kullanımına dikkat etmek gerekiyor. Her add_filter() ve add_action() çağrısı WordPress’in hook sistemine bir yük bindiriyor.

// KÖTÜ: Her sayfa yüklemesinde çalışan ağır işlemler
add_filter('the_content', function($content) {
    // Regex ile tüm içeriği tara - çok pahalı!
    preg_match_all('/[sku:[^]]+]/', $content, $matches);
    foreach ($matches[0] as $match) {
        $sku = str_replace(['[sku:', ']'], '', $match);
        $product = wc_get_product_id_by_sku($sku); // Her eşleşme için DB sorgusu!
        $content = str_replace($match, get_price_html($product), $content);
    }
    return $content;
});

// İYİ: Önbellekli ve optimize edilmiş versiyon
add_filter('the_content', function($content) {
    if (!str_contains($content, '[sku:')) {
        return $content; // Erken çıkış - shortcode yoksa işlem yapma
    }
    
    preg_match_all('/[sku:([^]]+)]/', $content, $matches);
    
    if (empty($matches[1])) {
        return $content;
    }
    
    // Tüm SKU'ları tek sorguda çek
    $skus = array_unique($matches[1]);
    $price_cache = [];
    
    foreach ($skus as $sku) {
        $cache_key = 'sku_price_' . sanitize_key($sku);
        $price = get_transient($cache_key);
        
        if (false === $price) {
            $product_id = wc_get_product_id_by_sku($sku);
            $product    = wc_get_product($product_id);
            $price      = $product ? $product->get_price_html() : '';
            set_transient($cache_key, $price, 30 * MINUTE_IN_SECONDS);
        }
        
        $price_cache[$sku] = $price;
    }
    
    foreach ($matches[0] as $index => $full_match) {
        $sku     = $matches[1][$index];
        $content = str_replace($full_match, $price_cache[$sku] ?? '', $content);
    }
    
    return $content;
});

Autoloaded Options Temizliği

WordPress’in wp_options tablosunda autoload = yes olan her kayıt her sayfa yüklemesinde belleğe yükleniyor. Eklentiler burayı kirletmeye bayılıyor.

# Autoload büyüklüğünü kontrol edin
wp eval "
global $wpdb;
$size = $wpdb->get_var("
    SELECT SUM(LENGTH(option_value)) 
    FROM {$wpdb->options} 
    WHERE autoload = 'yes'
");
echo 'Toplam autoload boyutu: ' . size_format($size) . PHP_EOL;
"

# En büyük autoload kayıtlarını listeleyin
wp eval "
global $wpdb;
$rows = $wpdb->get_results("
    SELECT option_name, LENGTH(option_value) as size
    FROM {$wpdb->options}
    WHERE autoload = 'yes'
    ORDER BY size DESC
    LIMIT 20
");
foreach($rows as $row) {
    echo $row->option_name . ': ' . size_format($row->size) . PHP_EOL;
}
"

Eklenti kendi seçeneklerini kaydederken autoload’ı düşünmek gerekiyor:

// Büyük veri veya nadiren kullanılan seçenekler için autoload'ı kapat
add_option('my_plugin_large_config', $data, '', 'no'); // 'no' = autoload etme

// Mevcut seçenekte autoload'ı değiştirme
$wpdb->update(
    $wpdb->options,
    ['autoload' => 'no'],
    ['option_name' => 'my_plugin_large_config']
);

// Tercih edilen modern yöntem
update_option('my_plugin_large_config', $data, false); // false = autoload yok

Gerçek Dünya Senaryosu: WooCommerce Eklentisi Optimizasyonu

Bir müşterinin WooCommerce mağazasında özel yazılmış bir eklenti günde 50.000 sipariş istatistik sorgusu üretiyordu. Sayfa başına ~15 sorgu, 3.000 günlük ziyaretçi. Sunucu yanıt süresi 4-6 saniye arasında gidip geliyordu.

Çözüm üç aşamalıydı:

Birinci aşama: Tüm istatistik sorgularını 5 dakikalık transient’larla önbelleğe aldık. Sorgu sayısı 50.000’den ~288’e düştü.

İkinci aşama: Arka planda çalışan bir Action Scheduler görevi, istatistikleri her 5 dakikada bir ön hesaplayıp option tablosuna yazıyor. Kullanıcılar artık sıfır sorgu yapıyor, sadece get_option() çağırıyor.

Üçüncü aşama: Redis object cache kurulumu. Option ve transient değerleri artık MySQL’e gitmek yerine doğrudan RAM’den dönüyor.

Sonuç: Sayfa yüklenme süresi 4.8 saniyeden 0.9 saniyeye indi. Sunucu CPU kullanımı %78’den %23’e geriledi.

# Değişiklik öncesi/sonrası karşılaştırması için basit benchmark
for i in {1..10}; do
    curl -o /dev/null -s -w "%{time_total}n" https://siteniz.com/magaza/
done | awk '{sum+=$1; count++} END {print "Ortalama: " sum/count " saniye"}'

Sonuç

WordPress eklenti performans optimizasyonu tek seferlik bir iş değil. Yeni bir eklenti kurarken, güncellenirken veya özel kod yazarken bu prensipleri uygulamak bir kültür meselesi.

Önceliklendirme sırasına göre bakarsak: veritabanı sorgu optimizasyonu en yüksek etkiyi en az maliyetle sunuyor. Ardından script kontrolü geliyor, çünkü front-end’e gönderilen gereksiz kaynaklar kullanıcının tarayıcısında işleniyor ve bu tamamen sysadmin’in kontrolünden çıkıyor. Object cache altyapısı ise çaba gerektiren ama getirisi yüksek uzun vadeli bir yatırım.

Eklenti geliştiriyorsanız yukarıdaki pattern’leri başından uygulayın. Hazır eklenti yönetiyorsanız Query Monitor ile başlayın, darboğazı tespit edin, sonra hedefli müdahale yapın. Kör optimizasyon hem zaman kaybı hem de yeni hataların davetiyesi.

Bir yanıt yazın

E-posta adresiniz yayınlanmayacak. Gerekli alanlar * ile işaretlenmişlerdir