CSS ile Görsel Lazy Loading: Intersection Observer Kullanımı
Tarayıcılar, <img> etiketlerine eklenen loading="lazy" niteliği sayesinde ekran dışındaki görselleri sayfa ilk yüklendiğinde indirmez; kullanıcı o alana yaklaştığında yükleme başlar. Bu yaklaşım çoğu durumda yeterlidir ve ek kod gerektirmez. Ancak bazı senaryolarda bu yerleşik özellik devreye giremez: CSS background-image ile tanımlanmış arka plan görselleri, data-src gibi özel niteliklerle kontrol edilen bileşenler ve carousel ya da sekme (tab) yapıları bunların başında gelir.
loading="lazy" yalnızca HTML <img> ve <iframe> öğelerine uygulanır. CSS arka plan görseli bir HTML niteliği değil, hesaplanan bir stil kuralıdır; tarayıcı bu görseli CSSOM hesaplamaları sırasında fark eder ve öğe görünür durumdaysa çoğunlukla hemen indirir. Büyük bir hero bölümünün altındaki içerik alanları CSS arka planlarıyla doluysa, bu görseller ekrana gelmeden önce bant genişliğini tüketebilir.
Intersection Observer API, bir öğenin viewport'la ya da belirli bir container'la kesişip kesişmediğini izler ve kesişme gerçekleştiğinde bir callback çalıştırır. Bu mekanizma, yüklenecek görseli yalnızca gözlemci tetiklendiğinde CSS sınıfına eklemek ya da src niteliğini değiştirmek için kullanılabilir; tarayıcının yerleşik lazy loading'e erişemediği her durumda devreye girebilecek bir araç olur.
loading="lazy" ne zaman yetersiz kalır?
<img loading="lazy"> belirleyici bir sorunun üstesinden gelir: DOM'daki görsellerin sıradan yüklenmesini geciktirir. Ancak şu durumlar bu yerleşik özelliğin kapsama alanı dışında kalır.
CSS background-image: background-image: url(...) ile tanımlanan görseller HTML niteliği değildir. Tarayıcı, bu görseli CSSOM hesaplamasının ardından indirir; öğe görünür (display: none değil) durumdaysa yükleme başlar, ekran dışında olup olmadığı dikkate alınmaz. Gizli bir öğenin arka planı yüklenmez, ama sınıf eklenerek gösterilen bir arka plan hemen yüklenmeye başlar.
Özel bileşenler ve framework yapıları: Bazı carousel, accordion veya tab bileşenleri görselleri data-src gibi özel niteliklere koyar; src niteliği boş bırakılır ya da bir yer tutucu (placeholder) atanır. Tarayıcının yerleşik lazy loading'i bu altyapıyı tanımaz. Gerçek src değeri JavaScript ile atandığında loading="lazy" zaten geç kalır.
Kontrol eksikliği: loading="lazy" için tarayıcının kullandığı eşik değeri, bağlantı hızına ve tarayıcı sürümüne göre değişir; bazı durumlarda görsel viewport'tan oldukça uzakta olsa bile önceden yüklenir. Bu davranış üzerinde geliştirici kontrolü yoktur. Intersection Observer ise eşik değerini, tetikleme mesafesini ve hangi öğelerin izleneceğini açıkça belirlemenize izin verir.
Intersection Observer API nasıl çalışır?
Intersection Observer, kaydırma (scroll) olayını dinleyen geleneksel yaklaşımların aksine ana iş parçacığını (main thread) bloke etmez; tarayıcı bunu asenkron olarak işler. Temel yapısı şöyledir:
const observer = new IntersectionObserver((entries) => {
entries.forEach((entry) => {
if (entry.isIntersecting) {
// Görsel yükleme işlemi buraya
observer.unobserve(entry.target);
}
});
}, {
rootMargin: '200px 0px',
threshold: 0
});
rootMargin değeri, tetikleme alanını viewport sınırlarının dışına taşır. 200px 0px ile görsel ekrana girmeden 200 piksel önce yükleme başlatılabilir; böylece kullanıcı o alana geldiğinde görsel zaten hazırdır. threshold: 0 ise öğenin tek bir pikseli bile viewport'a girdiğinde callback'in çalışması anlamına gelir.
observer.unobserve(entry.target) çağrısı kritik bir adımdır. Görsel yüklendikten sonra gözlemciyi kaldırmazsanız aynı öğe tekrar tekrar izlenmeye devam eder ve gereksiz bellek kullanımı oluşur. Yükleme tamamlanan öğeyi hemen gözlemden çıkarın.
Tüm gözlemlenecek öğeleri seçip observer'a eklemek için standart örüntü şöyledir:
document.querySelectorAll('[data-lazy]').forEach((el) => {
observer.observe(el);
});
CSS arka plan görseli için uygulama
CSS background-image lazy loading için iki katmanlı bir yaklaşım işe yarar: HTML'de arka plan URL'si tanımlanmaz, bunun yerine bir veri niteliği (data attribute) tutulur; JavaScript, öğe viewport'a girdiğinde bu URL'yi gerçek arka plan stiline dönüştürür.
HTML tarafında:
<div class="hero-bg" data-bg="gorseller/hero-arka-plan.webp">
<p>İçerik buraya</p>
</div>
CSS tarafında varsayılan durum ile yükleme sonrası durum:
.hero-bg {
background-color: #f0f0f0;
min-height: 400px;
}
.hero-bg.bg-loaded {
background-size: cover;
background-position: center;
}
JavaScript:
const bgObserver = new IntersectionObserver((entries) => {
entries.forEach((entry) => {
if (entry.isIntersecting) {
const el = entry.target;
const bgUrl = el.dataset.bg;
if (bgUrl) {
el.style.backgroundImage = `url(${bgUrl})`;
el.classList.add('bg-loaded');
}
bgObserver.unobserve(el);
}
});
}, { rootMargin: '150px 0px' });
document.querySelectorAll('[data-bg]').forEach((el) => {
bgObserver.observe(el);
});
data-bg niteliği URL taşır. Öğe görünür alana 150 piksel yaklaştığında arka plan stili dinamik olarak atanır ve bg-loaded sınıfı eklenerek CSS geçiş (transition) efektleri devreye alınabilir. data-bg yerine başka bir adlandırma da seçilebilir; önemli olan CSS'te URL'yi hiç tanımlamamak, sadece JavaScript'in ihtiyaç anında atayacağı noktaya bırakmaktır.
Görselin format seçimi bu aşamada sonucu doğrudan etkiler. Görsel kalitesini sıkıştırma sonrasında ölçmek, lazy loading kazanımını gerçekten gösteren karşılaştırmalar sunar; hangi formatın hangi senaryoda uygun olduğunu netleştirmeye yardımcı olur.
Özel bileşenler ve data-src kalıbı
Carousel, accordion veya dinamik sekme yapıları için <img> etiketleri kullanılıyorsa ama loading="lazy" yeterli olmuyorsa, data-src kalıbı devreye alınır. Öğenin gerçek src değeri boş bırakılır ya da küçük bir SVG yer tutucu atanır; görsel yalnızca gözlemci tetiklendiğinde gerçek kaynakla değiştirilir.
<img
data-src="urunler/sehpa-800w.webp"
src="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 800 600'/%3E"
width="800"
height="600"
alt="Ahşap sehpa, koyu ceviz rengi"
class="lazy-img"
/>
width ve height nitelikleri burada zorunludur; aksi hâlde tarayıcı görsel yüklenene kadar boyutu bilemez ve layout kayması (CLS) oluşur. SVG yer tutucu görsel ağırlıksız olduğundan ilk yükleme trafiğini artırmaz.
JavaScript tarafında:
const imgObserver = new IntersectionObserver((entries) => {
entries.forEach((entry) => {
if (entry.isIntersecting) {
const img = entry.target;
const src = img.dataset.src;
if (src) {
img.src = src;
img.removeAttribute('data-src');
}
imgObserver.unobserve(img);
}
});
}, { rootMargin: '200px 0px', threshold: 0 });
document.querySelectorAll('img.lazy-img').forEach((img) => {
imgObserver.observe(img);
});
data-src niteliği, görsel yüklendikten sonra kaldırılır; böylece öğe ikinci kez sorgulanırsa gözlemcinin çalıştığı belli olur. srcset desteği de aynı yaklaşımla eklenebilir: HTML'de data-srcset tutulur, JavaScript tetiklendiğinde hem src hem srcset atanır.
if (img.dataset.srcset) {
img.srcset = img.dataset.srcset;
img.removeAttribute('data-srcset');
}
Responsive görsel planlaması ve kırılma noktası seçimi hakkında daha fazla bilgi için farklı boyut planlaması üzerine yazıyı inceleyebilirsiniz; orada srcset ve sizes niteliklerinin nasıl kurgulanması gerektiği ayrıntılı ele alınmaktadır.
rootMargin ile tetikleme zamanını ayarlamak
Tetikleme erken gelirse fazla görsel yüklenir, geç gelirse kullanıcı boş alanla karşılaşır. İkisi arasındaki denge büyük ölçüde bağlantı hızına ve içerik yoğunluğuna bağlıdır; sabit bir "doğru değer" yoktur.
Pratik bir başlangıç noktası olarak hızlı bağlantılarda rootMargin: '100px 0px', yavaş bağlantılarda rootMargin: '300px 0px' kullanılabilir. Bağlantı hızını navigator.connection ile sorgulamak mümkündür; ancak bu API tüm tarayıcılarda desteklenmez ve salt bir rehber değer verir.
const margin = navigator.connection?.effectiveType === '4g'
? '100px 0px'
: '300px 0px';
const observer = new IntersectionObserver(callback, { rootMargin: margin });
Bu ayrımı yapmak zorunlu değildir. Sabit 200px 0px çoğu durumda makul bir sonuç verir. Önemli olan, değeri keyfi artırmaktan kaçınmaktır: çok geniş bir rootMargin gerçek lazy loading'i ortadan kaldırır ve kullanıcı görmeyeceği görselleri de indirir.
Birden fazla observer örneği oluşturmak yerine, farklı rootMargin değerleri gerektiren öğeleri iki ayrı observer'a bölmek daha temizdir. Örneğin, arka plan görselleri daha erken, ürün listeleri daha geç tetiklenebilir.
Gözlemci temizliği ve bellek yönetimi
Görsel yüklendikten sonra observer.unobserve(element) çağrısı yapılmadığında, gözlemci o öğeyi izlemeye devam eder. Tek sayfalık uygulamalarda (SPA) bu durum, sayfalar arası geçişlerde eski gözlemcilerin bellekte birikmesine yol açabilir. Her component'in temizlik aşamasında observer.disconnect() çağırılması gerekir.
// React örneği - useEffect cleanup
useEffect(() => {
const observer = new IntersectionObserver(callback, options);
elements.forEach((el) => observer.observe(el));
return () => observer.disconnect();
}, []);
Vanilla JavaScript projelerinde, sayfadan ayrılırken ya da bileşen kaldırıldığında disconnect() çağrısını yönetecek bir mekanizma kurmak gerekir. Yoksa gözlemciler bellekte kalmaya devam eder; bu durum kısa sayfa ziyaretlerinde fark yaratmasa da uzun oturumlarda bellek kullanımını artırabilir.
Aynı sayfada çok sayıda tembel yüklenen öğe varsa, her öğe için ayrı observer oluşturmak yerine tek bir observer'a tüm öğeleri kaydetmek tercih edilmelidir. Observer maliyetinin öğe sayısından değil, gözlemci sayısından kaynaklandığını bilmek önemlidir.
Bu yaklaşım ne zaman gereksiz ya da yanlış olur?
Intersection Observer, her görsel için tercih edilmesi gereken bir yöntem değildir. Yanlış kullanıldığında hem kodun karmaşıklığını artırır hem de performans sorunlarına yol açar.
Above-the-fold görseller için kullanmayın. İlk ekranda görünen hero görselleri, logo veya banner'lar lazy loading gerektirmez; aksine mümkün olduğunca erken yüklenmeleri beklenir. Bu görsellere loading="eager" ya da fetchpriority="high" eklemek, Intersection Observer eklemeye kıyasla çok daha uygun bir yaklaşımdır. LCP (Largest Contentful Paint) puanını etkileyen görseli geciktirirseniz Core Web Vitals skorları olumsuz etkilenir.
Az sayıda görsel için fazla mühendislik yapmayın. Sayfada beş ila on görsel varsa ve hepsi <img> etiketi kullanıyorsa, loading="lazy" niteliği yeterlidir. JavaScript karmaşıklığı eklemenin getirisi yoktur. Intersection Observer'ın değer yarattığı alan, onlarca arka plan görseli, büyük carousel'lar veya dinamik bileşenlerdir.
Server-Side Rendering (SSR) bağlamında dikkatli olun. SSR ortamlarında JavaScript'in ne zaman çalıştığına bağlı olarak ilk render'da görseller görünmeyebilir; SEO tarama botları yüklenmeyen görseller nedeniyle sayfayı eksik tarayabilir. Arka plan görseli SEO açısından kritikse, CSS çözümü yerine HTML tabanlı bir alternatif düşünülmelidir.
Görsellerin toplu olarak optimize edilmesi ve formatlanması sürecini de bu bağlamda göz önünde bulundurmak gerekir. Toplu dönüştürme akışını kurarken lazy loading stratejisiyle uyumlu dosya boyutları ve formatlar seçmek, iki katman arasında tutarlılık sağlar.
Tarayıcı desteği ve polyfill ihtiyacı
Intersection Observer API, tüm güncel tarayıcılarda desteklenir. Eski tarayıcılar için polyfill kullanmak gerekebilir, ancak yaygın kullanım kitlesi için bu ihtiyaç pratikte azalmıştır.
Polyfill eklemek yerine kademeli bir yaklaşım tercih edilebilir: Intersection Observer desteklenmiyorsa tüm görseller normal yolu izlese de sayfa işlevini yitirir. Bu, yalnızca desteklenmeyen tarayıcılarda lazy loading'i devre dışı bırakır; çoğu kullanıcı etkilenmez.
if ('IntersectionObserver' in window) {
setupLazyLoading();
} else {
document.querySelectorAll('[data-src]').forEach((img) => {
img.src = img.dataset.src;
});
}
Bu kademeli iyileştirme yaklaşımı, desteklenmeyen ortamlarda sayfanın çalışmaya devam etmesini sağlar. Görsel sürümleme ve eski dosyaların temizlenmesi gibi dosya yönetimi konularında görsel sürümleme yazısı pratik bir başlangıç noktası sunar.
Intersection Observer ile loading="lazy" rakip değil, tamamlayıcı araçlardır. HTML görselleri için yerleşik nitelik önce tercih edilmeli; yalnızca CSS arka planları, özel bileşenler veya kontrol gerektiren durumlar için JavaScript tabanlı çözüme geçilmelidir. Kod eklemek her zaman kazanım getirmez; yerleşik platformun yetmediği noktaları bulmak ve yalnızca orada müdahale etmek, gereksiz karmaşıklıktan kaçınmanın en iyi yoludur.
Büyük projelerde görsel optimizasyonu tek bir teknikle sınırlı kalmaz. Sıkıştırma sonrası kalite ölçümü ile lazy loading kazanımlarını nesnel biçimde değerlendirmek, hangi görsellerin gerçekten ertelenmeye değer olduğunu ortaya koyar. Dosya boyutları ve formatlar optimize edilmeden lazy loading uygulaması tek başına yeterli bir iyileştirme sağlamayabilir.