React.lazy ve Suspense, ilk duyulduğunda neredeyse sihir gibi gelir: büyük bir bileşeni ayrı bir chunk'a taşırsınız, fallback yazarsınız, sayfa başlangıç yükü hafifler. Gerçek bu kadar temiz değil. İkisini birlikte kullandığınızda ne zaman gerçek bir kazanım elde ettiğinizi, ne zaman yalnızca karmaşıklık kattığınızı anlamak için hem bundle mekanizmasına hem kullanıcı deneyimine bakmak gerekir.

Bir uygulama büyüdükçe, her şeyin başlangıç bundle'ına girmesi kaçınılmaz hale gelir; yönetim paneli, grafik kütüphanesi, modal içerikleri, form adımları. Kullanıcıların büyük çoğunluğu bu bileşenlerin yarısını asla açmayabilir. React.lazy bu durumu çözmek için var: ilgili bileşeni yalnızca ihtiyaç duyulduğunda indirip çalıştırırsınız. Suspense ise indirme sırasında ekranda ne gösterileceğini tanımlamanızı sağlar.

Kazanım gerçek; ama bedelsiz değil. Bölme kararı yanlış bileşene uygulandığında ya da fallback ihmal edildiğinde, kullanıcı "bir şey yükleniyor" çıkmazına girer; bu, başlangıç yükünü kısaltmak yerine deneyimi kötüleştirmek anlamına gelir. Aynı araç, sınır doğru çizilince görünmez kalır, yanlış çizilince bekleme üretir.

React.lazy ve Suspense arka planda nasıl çalışır?

React.lazy bir dinamik import döndürür: const Modal = React.lazy(() => import('./Modal')). Bu satır çalıştığında network isteği başlamaz; bileşen ilk kez render edilmek üzere ihtiyaç duyulduğunda başlar. Suspense sınırı, beklenme süresini yakalar ve fallback'i ekrana verir.

Bundle boyutu açısından değerlendirildiğinde, Webpack ya da Vite gibi araçlar dinamik import gördüğü anda ilgili modülü ayrı bir chunk dosyasına ayırır. Başlangıç bundle'ına girmeyen bu dosya, istek geldiğinde ayrıca indirilir. Boyut farkı doğrudan chunk içeriğine bağlıdır; küçük bir bileşen 2-3 KB kurtarır, bağımlılıklarıyla birlikte gelen bir grafik kütüphanesi 80-100 KB. Paylaşılan bağımlılıklar vendor chunk'a gider ve bölme işleminden bağımsız kalır; aynı kütüphaneyi kullanan iki farklı bileşeni ayrı chunk'lara taşımak kütüphaneyi iki kez indirtmez.

Suspense sınırı render ağacında herhangi bir yere yerleştirilebilir; ama konumu kritik. Geniş bir Suspense, bekleme süresinde büyük bir alanı boş bırakır. Dar tutulmuş bir sınır, yalnızca yüklenen parçayı etkiler ve sayfa geri kalanı kullanılabilir kalır.

Hangi bileşeni bölmek kazandırır, hangisini bölmek geri teper?

Her bileşeni React.lazy'ye taşımak anlamsız. Bölme kararını kimin yararlanacağına göre vermek gerekir.

Bölmeye değer bileşenler belirli koşulları paylaşır: ilk render'da görünmezler, büyük bağımlılık taşırlar, kullanıcının yalnızca belirli bir akışta eriştiği içeriklerdir. Ayar modali, raporlama ekranı, zengin metin editörü veya PDF görüntüleyicisi bu kategoriye girer. Kullanıcıların büyük çoğunluğu bu bileşenlere ulaşmadan uygulamayı kapatır; bu yüzden başlangıç yüküne dahil etmek anlamsızdır.

Bölme geri tepeceği yerler daha nettir. Header bileşeni, navigasyon, sayfanın ilk görünür içeriği (above the fold), giriş formu ve uygulama shell'i React.lazy'ye taşınmamalıdır. Bu bileşenler ilk render'da görünmesi gereken parçalardır. Lazy yüklendiğinde kısa da olsa fallback gösterilir; kritik içerikte fallback, layout kayması ve algılanan gecikme yaratır.

Orta boy bileşenler için karar, sayfanın ne sıklıkta ziyaret edildiğine ve bileşenin ne kadar erken ihtiyaç duyulduğuna göre değişir. Sık açılan, 5 KB altındaki bileşenler için bölme overhead'i (ayrı HTTP isteği, chunk parse maliyeti) tasarruftan büyük olabilir. Kod bölme ile sayfa sadeleştirme arasındaki seçimi doğru yapmak bu tür ara durumlarda belirleyici olur.

Fallback tasarımı: kullanıcı ne görür, ne hisseder?

Fallback, Suspense'in en az konuşulan ama en çok hissedilen parçasıdır. <Suspense fallback={<div>Yükleniyor...</div>}> yazmak teknik olarak çalışır; kullanıcı açısından bakıldığında ise genellikle kötü bir deneyim üretir.

İyi bir fallback iki soruyu yanıtlamalı: kullanıcı içeriğin geleceğini anlıyor mu, ve mevcut layout bozuluyor mu? Yalnızca metin tabanlı "yükleniyor" ibaresi ikinci soruyu yanıtlamaz. İçerik tam boyutuyla geldiğinde layout aniden değişir; bu cumulative layout shift (CLS) metriğini etkiler ve özellikle mobilde dikkat dağıtıcı bir sıçramaya yol açar.

Skeleton ekranlar fallback olarak kullanıldığında iki şey olur: layout stabilitesi korunur çünkü içerik gerçek boyutuna yakın bir yer kaplar, ve kullanıcı beklemenin bir sonucu olduğunu sezgisel olarak görür. Skeleton da masrafa yol açar; aşırı karmaşık bir skeleton bileşeni kendi başına render maliyeti taşır. Basit, boyutlandırılmış gri bloklar çoğu durumda yeterlidir.

Fallback süresi tahmin edilemez. Hızlı bağlantıda 100 milisaniyede tamamlanan bir yükleme, yavaş bağlantıda 2-3 saniyeye çıkar. Fallback hem anlık görünüp kaybolmaya hem de uzun süreli gösterime hazır olmalıdır; bu iki senaryo için aynı tasarım kararı çoğu zaman çalışır. 100 milisaniyenin altındaki fallback'ler kullanıcılar tarafından hemen fark edilmez; 300 milisaniyeyi geçen bekleme ise bilinçli olarak hissedilir ve skeleton bu eşikte görsel sürekliliği korumanın asıl aracı haline gelir.

Network waterfall: iç içe Suspense ne zaman tehlikeli olur?

İç içe Suspense sınırları, görünürde makul fakat pratikte sorunlu bir yapı üretebilir. Bir sayfa bileşeni lazy yüklenir, içindeki alt bileşen de lazy yüklenir, o alt bileşen de kendi içinde bir veri isteği başlatır. Her adım bir öncekinin tamamlanmasını bekler; bu, network waterfall'dur.

// Her adım sırayla başlar, paralel değil
const Page = React.lazy(() => import('./Page'));
// Page render edilince Widget keşfedilir:
const Widget = React.lazy(() => import('./Widget'));
// Widget render edilince fetch başlar

Bu yapıda tarayıcı önce Page chunk'ını indirir, render eder, ardından Widget chunk'ını indirir, render eder, sonra veri isteğini başlatır. Üç adım zincirlenir. Hızlı bir bağlantıda fark edilmeyebilir; yüksek gecikme (latency) olan ortamlarda toplam bekleme süresi katlanarak artar.

Çözüm, chunk'ları mümkün olduğunca erkenden tetiklemektir. Bir bileşeni render etmeden önce import('./Widget') çağrısı yapılması, chunk indirmeyi öne alır. Route bazlı bölme bu sorunun doğal çözümüdür: kullanıcı bir sayfaya geçmeden önce o sayfanın chunk'ı arka planda yüklenebilir. Vite yapılandırmasında preload direktifleri bu süreci otomatikleştirir ve waterfall riskini azaltır.

Suspense sınırlarını nereye koymalı?

Sınır ne kadar geniş tutulursa bekleme sırasında o kadar büyük bir alan boş kalır; ne kadar dar tutulursa her küçük bileşen ayrı ayrı "flaş" üretir, içerik art arda farklı animasyonlarla gelir. İkisi de kötü.

Pratik bir kural: Suspense sınırını anlamlı bir içerik birimi etrafına koyun. Tek bir kart bileşeni başlı başına bir sınır kurmak için çoğu zaman yeterince anlamlı değildir; kartların listesi ya da sayfanın belirli bir bölümü daha mantıklı bir yer oluşturur. Kullanıcı neyi beklediğini sezgisel olarak anlamalı; bu da o alanın boyutuna uygun bir skeleton ile sağlanır.

Birden fazla lazy bileşen aynı Suspense altında toplanabilir; bu, hepsini tek bir fallback ile sarar. Tüm parçalar hazır olana kadar fallback görünür, hazır olduğunda hepsi birden ortaya çıkar. Ayrı Suspense sınırları ise ayrı hazır olmayı, dolayısıyla ayrı flaşları sağlar. Çok bileşenli uygulamalarda render maliyetini yönetirken Suspense sınırları re-render yayılmasını kısıtlamanın da bir aracı haline gelebilir; yalnızca yükleme değil, güncellenme davranışı da bu sınırlara göre şekillenir.

Ölçmeden karar vermek: hangi metrikler rehber olur?

Doğru metriğe bakmadan lazy bölme uygulamak çoğu zaman boş bir iyimserlik üretir. Değişiklik öncesi ve sonrası iki grup ölçüm yeterli çoğu durum için.

İlk grup başlangıç yüküyle ilgilidir: JavaScript bundle boyutu (byte olarak), ilk anlamlı çizim (FCP), etkileşime hazır olma süresi (TTI). Bu metrikler lazy bölmenin başlangıca etkisini gösterir. Eğer bölünen bileşen zaten yüklenmiş bağımlılıkları paylaşıyorsa, bağımlılıklar vendor chunk'ta zaten yer alıyordur ve gerçek tasarruf beklentinin altında kalabilir.

İkinci grup geçiş deneyimiyle ilgilidir: bileşen yükleme süresi (chunk indirme ve parse süresi toplamı), fallback görünüm süresi, CLS değeri. Bu grup genellikle göz ardı edilir; ama kullanıcının hissettiği gecikme tam buradadır. Yayına çıkmadan önce yapılan son kontrolde her lazy bileşenin yavaş bağlantı simülasyonuyla test edilmesi, fallback davranışının gerçekçi görüntüsünü verir. Chrome DevTools'ta "Slow 3G" ile bileşeni ilk kez açtığınızda fallback birkaç saniye boyunca görünür; tasarımın bu koşula uygunluğu bu şekilde anlaşılır.

React.memo gibi optimizasyonlarda olduğu gibi burada da ölçüm olmadan yazılan kod genellikle ya fazla, ya eksik olur. Bileşen sık açılıyorsa, küçükse ya da kritik bir akışın ortasındaysa, lazy taşıma getiri yerine gecikme ekler. Bölme kararı bileşen boyutuyla değil, bileşenin ne zaman ihtiyaç duyulduğuyla başlar.

Ölçüm iki yönde de karar verir: başlangıç bundle'ı gerçekten küçülmediyse bölme işe yaramamıştır; küçüldü ama fallback saniyelerce kaldıysa kullanıcı o kazancı hissetmez. Aynı araç, sınır doğru çizilince görünmez kalır; yanlış çizilince layout kayması ve zincirleme istek üretir.

Fallback, geçici bir yer tutucu değil; deneyimin bir parçasıdır. Skeleton boyutu, sınır konumu ve waterfall riski birbirinden bağımsız değil; biri ihmal edildiğinde diğerinin getirisi yarım kalır. Araç doğru seçilip doğru konumlandırıldığında görünmez olur: kullanıcı ne lazy bileşeni bilir ne de Suspense sınırını, yalnızca sayfanın hızlı açıldığını ve geçişlerin pürüzsüz olduğunu fark eder.