Başlangıç noktası
Mağaza: OpenCart 4.0.2.3, 38.000 ürün, 11 dil, 62 eklenti, Apache’li ve nesne önbelleği olmayan 4 çekirdekli bir VPS. Kategori sayfaları sakin zamanlarda ilk byte için 4,1 saniye alıyor, kampanyalarda zaman aşımına düşüyordu. Mağaza sahibinin brief’i tek cümleydi: “Black Friday’den önce hızlandırın.”
- TTFB (kategori)
- 4,1 s
- Ürün
- 38.000
- Eklenti
- 62
Bunu yeniden tasarım değil, bir olay gibi ele aldık: ölç, tek bir şeyi değiştir, tekrar ölç. Aynı değişiklik, tam veritabanı kopyasıyla kurulmuş bir staging ortamında bir gün çalışmadan canlı mağazaya dokunmadık. Aşağıda yaptığımız sıra ve her adımın kazandırdığı var.
0. Hiçbir şeye dokunmadan önce ölçmek
Gelen “OpenCart yavaş” taleplerinin neredeyse hepsinin yanında bir teori olur — genellikle hosting. Teoriyi kabul ya da reddetmeden önce dört sayıya ihtiyacımız var: en yavaş sayfanın ilk byte süresi, o istek içindeki veritabanı süresi, istek başına sorgu sayısı ve tarayıcıdaki Largest Contentful Paint. İlkini toplamak on saniye sürüyor:
curl -o /dev/null -s -w "ttfb=%{time_starttransfer}s total=%{time_total}s\n" \
"https://magaza.example/index.php?route=product/category&path=57"Bunu farklı saatlerde yirmi kez döngüyle çalıştırmak, sayfanın her koşulda mı yoksa yalnızca önbellek soğukken mi yavaş olduğunu söyler. Her koşulda yavaştı. Ardından bir iş günü boyunca MySQL slow query log’unu bilinçli olarak düşük bir eşikle açtık; böylece sayfa başına yüzlerce kez çalışan “yeterince hızlı” sorgular da listeye girdi:
SET GLOBAL slow_query_log = 'ON';
SET GLOBAL long_query_time = 0.5;
-- bir gün sonra, kabuktan:
mysqldumpslow -s t -t 10 /var/log/mysql/mysql-slow.logAynı anda `storage/logs/error.log` dosyasını okuduk. Bu mağazada 240 MB’tı ve üç eklentiden gelen PHP 8 deprecation uyarılarıyla doluydu — yavaşlığın sebebi değil ama hangi eklentilerin hâlâ PHP 7 için yazılmış olduğunun güvenilir bir haritası.
- En yavaş kategori, ürün ve arama sayfasının ilk byte süresi.
- Tek çalıştırma süresine değil, toplam süreye göre en yavaş on sorgu.
- İstek başına sorgu sayısı — bir kategori sayfasında 300’ün üstü, bir yerde indexsiz bir döngü var demektir.
- Kısıtlanmış bir tarayıcı profilinde LCP ve toplam transfer edilen byte.
1. Veritabanı: eksik indexler
OpenCart’ın varsayılan şeması birkaç bin ürün için yeterli. 11 dilli 38.000 üründe product_description ve product_to_category join’leri milyonlarca satır tarıyordu. Slow query log ilk üç sorgunun veritabanı süresinin %61’ini oluşturduğunu gösterdi; EXPLAIN her birinde tam tablo taraması olduğunu doğruladı.
ALTER TABLE oc_product_to_category ADD INDEX idx_cat_prod (category_id, product_id);
ALTER TABLE oc_product_description ADD INDEX idx_lang_name (language_id, name(64));
ALTER TABLE oc_product ADD INDEX idx_status_sort (status, sort_order, date_available);Üç index, TTFB 2,6 saniyeye düştü. Ayrıca mağaza açıldığından beri hiç temizlenmemiş iki tablodan 1,9 milyon satır sildik — OpenCart her istekte ikisine de yazar ve hiçbiri otomatik olarak budanmaz.
DELETE FROM oc_customer_online WHERE date_added < NOW() - INTERVAL 7 DAY;
DELETE FROM oc_session WHERE expire < NOW();
OPTIMIZE TABLE oc_customer_online, oc_session;Bu temizlik artık gecelik bir cron işi olarak çalışıyor. İki yıldan uzun süredir yayında olan mağazalarda eksik bulduğumuz en yaygın tek şey bu.
2. Önbellek: dosya sistemi yerine Redis
OpenCart’ın dosya önbelleği `storage/cache/` içine dakikada binlerce küçük dosya yazıyor, gecelik bir cron da tüm klasörü siliyordu — yani her sabah ilk birkaç yüz ziyaretçi tüm katalog önbelleğini elleriyle yeniden kuruyordu. Önbellek motorunu Redis’e çevirmek ve kategori ile üretici verisinin süresini uzatmak disk I/O’sunun çoğunu ortadan kaldırdı, sabah zirvesi kayboldu.
3. Kaldırdığımız iki eklenti
Bir “ilgili ürünler” modülü her kategori sayfasında ürün başına indexsiz bir sorgu çalıştırıyordu — 40 ürünlük bir listede sayfa başına 40 sorgu. Bir SEO modülü de oc_seo_url tablosunu kullanmak yerine her istekte tüm URL’leri regex zinciriyle yeniden yazıyordu. İkisi de çekirdek dosyaları yamalamak yerine OpenCart 4 event’lerine bağlanan daha hafif uygulamalarla değiştirildi.
Eklentileri sabit bir sırayla inceleriz, çünkü canlı mağazada rastgele bir şeyler kapatmak bir hafta sonunu kaybetmenin yoludur:
- 1Mağazayı tam veritabanı dökümüyle staging’e kopyala; sorgu profili gerçekçi olsun.
- 2Tek bir eklentiyi kapat, önbelleği ısıt, en yavaş üç sayfada TTFB ve sorgu sayısını ölç.
- 3Tekrar aç ve sıradakine geç — asla iki eklentiyi aynı anda kapatma, yoksa farkı kimseye yazamazsın.
- 4100 ms’den fazla kazandıran her şey için karar ver: değiştirilebilir mi, yeniden yazılabilir mi, yoksa gereksiz mi.
İki eklenti aralarında 1,3 saniye tutuyordu. İkisi de kaldırıldıktan sonra TTFB: 1,3s.
4. Görseller ve frontend
`image/cache/` her temizlendiğinde küçük görseller anında yeniden üretiliyordu ve aynı gecelik cron bunu yapıyordu. Günün ilk ziyaretçisi pratikte tarayıcı sekmesinde toplu bir görsel işi çalıştırıyordu.
- Her katalog küçük görsel boyutunu bir kez önceden ürettik ve klasörü planlı olarak silmeyi bıraktık.
- Eski istemciler için fallback ile birlikte Nginx üzerinden WebP sunduk.
- Görseller yüklenirken düzenin kaymaması için width ve height niteliklerini ekledik.
- Ana sayfadaki dört karusel dahil, ekran altındaki her şeyi lazy-load yaptık.
Largest Contentful Paint 5,8s’den 1,4s’ye indi; bir kategori listesinin sayfa ağırlığı 4,2 MB’tan 900 KB’a düştü.
5. Sunucu: Nginx, PHP-FPM, OPcache
Son adım Apache’yi Nginx + PHP-FPM ile değiştirmek ve OPcache’e bu büyüklükte bir kod tabanına yetecek bellek havuzu vermekti. Binlerce PHP dosyası olan bir mağazada varsayılan 128 MB’lık havuz, dosyaları sessizce atıp her istekte yeniden derler.
opcache.memory_consumption=256
opcache.interned_strings_buffer=32
opcache.max_accelerated_files=30000
opcache.validate_timestamps=0
opcache.save_comments=1PHP-FPM şablona göre değil gerçek bellek kullanımına göre boyutlandırıldı: yük altında ortalama işlem boyutu ölçüldü, ardından tüm worker’lar RAM’in yaklaşık %70’ine sığacak şekilde `pm.max_children` ayarlandı; `pm.max_requests` ile de yavaş bir sızıntı önem kazanmadan worker’lar geri dönüştürülüyor. Cloudflare yalnızca statik dosyalar için öne alındı — tam sayfa önbelleği yok, çünkü sepet ve para birimi seçici aynı HTML’in içinde yaşıyor.
TTFB sakin zamanlarda 0,6s’ye oturdu, Black Friday zirvesinde 1,1s’nin altında kaldı — hem de mağazanın başladığı aynı 4 çekirdekli VPS üzerinde.
- TTFB (kategori)
- 0,6 s
- LCP
- 1,4 s
- Dönüşüm
- +%22
Bilerek yapmadıklarımız
Bir performans çalışmasının yarısı, iş reddetmektir. Şunlar masadaydı ve masada kaldı:
- Tema yeniden yazımı yok. Darboğaz şablon değildi ve onu yeniden yazmak yukarıdaki her ölçümü sahipsiz bırakırdı.
- Indexler düzelmeden daha büyük sunucuya geçiş yok — donanım, aylık dört katı maliyetle sorunu bir yıl daha saklardı.
- Sepet aynı yanıtın içindeyken mağazanın önüne tam sayfa önbellek yok.
- Platform geçişi yok. 0,6 saniyede yanıt veren bir mağazanın OpenCart’tan ayrılmasına gerek yok.
Çıkarımlar
Bir şeye dokunmadan önce ölçün. OpenCart “yavaşlığının” çoğu platform değil, birkaç sorgu ve bir iki eklentidir. Ucuz ve geri alınabilir olanları — index, temizlik ve önbellek — önce yapın; yeni hosting’i ancak ondan sonra düşünün. Yukarıdaki her adım bir dakikanın altında geri alınabilir; zaten bu yüzden en yoğun çeyrekte, canlı bir mağazada tek tek yayına alabildik.
Tüm çalışma dört güne yayılan 11 faturalanan saat sürdü: üç saat ölçüm, beş saat veritabanı ve eklentiler, üç saat sunucu profili.
Kategori sayfalarınız iki saniyenin üstündeyse ya da sipariş tablosu büyüdükçe admin kullanılamaz hâle geldiyse, mağaza adresinizi ve hosting bilgilerinizi bize yazın. Aynı ölçüm turuyla başlıyoruz; herhangi bir iş planlanmadan önce rakamları ve net bir öneri listesini alıyorsunuz. Çalışma lansman ücretiyle faturalanır: 10$ / saat + KDV.
Hız, caching ve CDN mimarisi; Core Web Vitals ve profilleme.