Streaming SSR, HTML'i parçalar halinde göndererek tarayıcının shell'i alır almaz kaynak yüklemeye başlamasını sağlar. React 18 Suspense ile yavaş veri bekleyen bileşenler askıya alınır, hızlı içerik öne çekilir ve FCP belirgin biçimde kısalır.

Streaming SSR Nedir ve Performansa Etkisi Nedir?

Sunucu tarafı render (server-side rendering, SSR) bir sayfanın HTML'ini sunucuda üretip tarayıcıya gönderir; bu yaklaşım arama motoru erişimi ve ilk yükleme hızı bakımından istemci tarafı render'a göre avantaj sağlar. Ancak klasik SSR'da tarayıcı, tüm HTML tamamlanana kadar tek bir bayt bile almaz. Veri tabanı sorgusu veya harici API çağrısı uzadığında bu bekleme TTFB'ye (Time to First Byte) doğrudan yansır ve ekran kullanıcıya boş görünür.

Streaming SSR bu beklemeyi ortadan kaldırmak için HTML'i tek bir yanıt yerine parçalar halinde gönderir. Tarayıcı ilk parçayı aldığı anda görüntülemeye başlar; geri kalan içerik sunucu hazırladıkça akar. React 18 ile gelen renderToPipeableStream ve Suspense entegrasyonu bu modeli bileşen düzeyinde yönetmeyi mümkün kılar; yavaş veri bekleyen bileşenler askıya alınır, hızlı içerik öne çekilir.

TTFB ile FCP (First Contentful Paint) arasındaki süre, streaming'den en çok kazanılan metriktir. Hangi parçaların önce gönderileceği, hangi bileşenlerin Suspense'e alınacağı ve sonucun nasıl doğrulanacağı, shell'in ne kadar erken boyandığına bakılarak okunur.

Klasik SSR'da darboğazlar nereden kaynaklanır?

Klasik SSR akışı şu sırayla ilerler: istek gelir, sunucu tüm bileşenler için gerekli veriyi toplar, HTML string'ini üretir, ardından tarayıcıya tek bir blok olarak gönderir. Veri toplama ve render süreci birbirine kilitlenmiştir; birindeki gecikme diğerini de durdurur.

Bir ürün sayfası düşünün. Ürün başlığı ve görseli CDN'den birkaç milisaniyede gelir, ama kullanıcıya özel öneri listesi, stok durumu ve yorumlar ayrı servislerden toplam 300-600 ms sürebilir. Klasik SSR'da bu sürenin tamamı tarayıcıya ilk byte ulaşmadan geçer; HTML'in tamamı hazır olmadan hiçbir şey gönderilmez.

Hızlı veri, yavaş verinin arkasında bekler. Statik navigasyon, sayfa başlığı ve şablon HTML'i milisaniyeler içinde hazır olabilir; ama bunlar yavaş API çağrısı bitmeden tarayıcıya ulaşamaz. Sorun verinin kendisi değil, zorunlu sıralı bekleme modelidir. Hazır olanı erkenden göndermek için yapının değişmesi gerekir.

Streaming SSR nasıl çalışır?

Streaming SSR, HTTP chunked transfer encoding'i kullanarak HTML'i parçalar halinde gönderir. Tarayıcı yanıtın tamamlanmasını beklemez; ilk chunk geldiğinde HTML'i parse etmeye, CSS ve font isteklerini başlatmaya, JavaScript'i indirmeye başlayabilir.

React 18'de bu modelin bileşen düzeyinde kontrolü Suspense ile sağlanır. Suspense bir bileşeni askıya alır ve o bileşenin verisi hazır olana kadar yerine bir fallback HTML gönderir. Veri geldiğinde sunucu eksik parçayı akışın devamında iletir; tarayıcı bunu ilgili placeholder ile değiştirir.

Akış sırası şöyledir: sunucu <head>, navigasyon ve hızlı bileşenleri hemen gönderir. Yavaş bileşenler için Suspense fallback'i - genellikle bir loading skeleton - yerleştirilir. Yavaş bileşenin verisi sunucuda hazırlandığında küçük bir chunk gönderilir ve bu snippet placeholder'ı asıl içerikle değiştirir. Akış kapanır.

Tarayıcı <head>'i aldığı anda preload taraması başlar ve kritik kaynaklar keşfedilir. Yavaş verinin bitmesi beklenmez. Streaming'in esas kazanımı buradadır: veri geç de gelse kaynak keşfi zamanında başlar ve FCP için gereken ilk görünür içerik çok daha erken gelir.

React 18 Suspense ve renderToPipeableStream

React 17 ve öncesinde sunucu tarafı render için kullanılan renderToString tüm bileşen ağacını senkron olarak işler ve tek bir string döndürür; streaming desteği yoktur. React 18, renderToPipeableStream (Node.js için) ve renderToReadableStream (edge runtime için) ile bu modeli değiştirdi.

renderToPipeableStream Node.js stream API'siyle doğrudan çalışır:

import { renderToPipeableStream } from 'react-dom/server';

app.get('*', (req, res) => {
  res.setHeader('Content-Type', 'text/html; charset=utf-8');

  const { pipe, abort } = renderToPipeableStream(
    <App url={req.url} />,
    {
      bootstrapScripts: ['/client.js'],
      onShellReady() {
        res.statusCode = 200;
        pipe(res);
      },
      onShellError(err) {
        res.statusCode = 500;
        res.send('<h1>Sunucu hatası</h1>');
      },
    }
  );

  req.on('close', abort);
});

onShellReady callback'i, Suspense fallback dahil shell HTML'i hazır olduğunda tetiklenir ve pipe(res) ile stream başlar. bootstrapScripts ile belirtilen istemci JS'i stream tamamlanmadan önce tarayıcıya bildirilir; React 18'in selective hydration özelliği görünürdeki bileşenleri önce hydrate ederek sayfa daha erken etkileşime girer.

Bileşen tarafında Suspense sarması şöyledir:

function ProductPage() {
  return (
    <>
      <Header />
      <ProductTitle />
      <Suspense fallback={<ReviewsSkeleton />}>
        <Reviews />
      </Suspense>
      <Suspense fallback={<RecommendationsSkeleton />}>
        <Recommendations />
      </Suspense>
    </>
  );
}

Header ve ProductTitle Suspense dışındadır; sunucu bunları hemen gönderir. Reviews ve Recommendations veri bekliyorsa skeleton önce gider, asıl içerik sonra akar. Her Suspense sınırı bağımsız çözümlenir: yorumlar hazırsa, öneriler beklerken yorumlar gönderilebilir.

TTFB ile FCP arasındaki süreyi kısaltmak

Streaming SSR'ın ölçüme en çok yansıyan etkisi FCP'dedir. TTFB değeri streaming ile mutlaka düşmez; sunucunun ilk byte'ı göndermesi için shell'in hazırlanması gerekir ve bu süre mimariye göre değişir. Shell tarayıcıya ulaşır ulaşmaz CSS ve kritik kaynaklar yüklenmeye başladığından FCP öne çekilir.

Klasik SSR'da TTFB ile FCP arasındaki süre genellikle kısadır çünkü tarayıcı tüm HTML'i tek seferde alır ve hemen render eder. Streaming'de bu aralık uzayabilir, ama FCP'nin mutlak değeri düşer; tarayıcı görünür içeriği daha erken çizer. Metrik kıyaslamasında yalnızca TTFB'yi izlemek yanıltıcı olur; FCP ve LCP birlikte değerlendirilmeli.

Aynı sayfanın iki versiyonunu yan yana ölçtüğünüzde sık görülen örüntü şudur: streaming'de TTFB bazen birkaç on milisaniye yükselir, çünkü shell'in hazırlanması ek bir iştir; FCP ise tarayıcı ilk HTML parçasını alır almaz düşer. Kullanıcı deneyimi bakımından belirleyici olan, o erken boyamadır.

Shell'i mümkün olduğunca hafif tutmak bu dengeyi iyileştirir. Shell genellikle <head>, kritik CSS, navigasyon ve sayfa iskeletini kapsar. Veri bağımlılığı olmayan tüm bu parçalar Suspense dışında bırakılmalı; sunucu bunları veri beklemeksizin hemen göndermeli. Yavaş bileşenler ne kadar iyi izole edilirse shell o kadar erken yola çıkar.

Hangi içerikler önce gönderilmeli?

Streaming SSR'ın etkinliği büyük ölçüde hangi bileşenlerin Suspense içine alındığına bağlıdır. Kural basit: kullanıcıya görünen ve veriye bağımlı olmayan her şeyi shell'e koy, veri bekleyen her şeyi Suspense'e al.

Önce gönderilmesi gerekenler şunlardır: sayfa <head> (meta etiketler, kritik CSS referansları, preload link'leri), navigasyon ve header, sayfa başlığı ve statik üst içerik, breadcrumb ve benzeri yapısal bileşenler. Bunlar veri sorgusu gerektirmez ve LCP adayı olabilecek görseller için erken preload başlatma fırsatı sunar.

Suspense'e alınmaya uygun bileşenler şunlardır: kullanıcıya özel veriler (sepet, öneri listesi, bildirimler), harici API'ye bağlı içerikler (stok durumu, fiyat), ağır veri tabanı sorgusu gerektiren listeler, gerçek zamanlı veriye bağlı metrikler. Bu bileşenler için bir skeleton veya minimal fallback tasarlamak önemlidir; boş fallback kullanıcıya sayfa bozukmuş izlenimi verebilir.

İç içe Suspense sınırları da kullanılabilir. Bir ürün kartı listesi genel bir Suspense içindeyken her kartın yorumları ayrı bir Suspense'te olabilir. Granülerlik arttıkça kısmi içerik daha erken gönderilebilir; ancak çok fazla Suspense sınırı sunucuda ek yük oluşturur. Her bileşen için ayrı Suspense açmak yerine anlamlı gruplamalar tercih edilmeli.

Streaming SSR ne zaman gereksiz veya ters etki yapar?

Streaming her SSR senaryosunda net kazanım sağlamaz. Kararı yapıya ve içeriğe göre vermek gerekir.

Sayfanın tüm içeriği zaten milisaniyeler içinde hazırsa streaming'in getirisi sıfıra yaklaşır. Statik blog sayfaları, çok az veri içeren landing page'ler veya zaten CDN'de önbelleklenen SSR çıktıları buna örnektir. Streaming eklemek bu durumlarda karmaşıklığı artırır, ama TTFB veya FCP'de kayda değer bir fark yaratmaz.

Ters etki riski de vardır. Streaming ortamında bazı HTTP ara katmanları - eski proxy'ler, belirli yük dengeleyiciler veya bazı CDN yapılandırmaları - chunked transfer encoding'i tamponlar; tarayıcıya ulaşmadan önce tüm yanıtı bekleyebilirler. Böyle bir ortamda streaming mimarisi kurarsınız ama tarayıcı farkı görmez. CDN veya proxy katmanının streaming'e uygun olup olmadığını önceden test etmek gerekir.

CMS tabanlı yapılarda streaming'i sonradan eklemek de düşündüğünüzden daha zordur. Mevcut veri katmanı Suspense ile uyumlu değilse her bileşeni yeniden yazmak gerekebilir. Next.js App Router gibi frameworkler bu entegrasyonu büyük ölçüde halleder, ama özel SSR kurulumlarında Suspense uyumu elle sağlanmalıdır. Edge runtime'da ise cold start süresi shell'in ne kadar erken gönderilebileceğini etkiler; düzinelerce veri kaynağından beslenen sayfalar için anlamlıdır, tek kaynaklı basit sayfalar için fazla mühendislik olur.

Streaming etkisini ölçmek ve doğrulamak

Streaming SSR'ın gerçekten çalıştığını doğrulamanın en kolay yolu Chrome DevTools'un Network sekmesidir. İlgili HTML yanıtını seçin ve Timing başlığına bakın; "Content Download" süresinin uzun, "Waiting (TTFB)" süresinin görece kısa olması streaming'in aktif olduğunu gösterir. Yanıt anında bitiyor ve tek bir boyut gösteriyorsa muhtemelen tamponlanıyordur.

Waterfall grafiğini inceleyin: streaming çalışıyorsa HTML indirme süreci boyunca CSS, font ve JavaScript istekleri zaten başlamış olmalıdır. Tüm alt kaynaklar HTML bittikten sonra başlıyorsa shell tarayıcıya geç ulaşıyor demektir ve Suspense sınırlarını yeniden gözden geçirmek gerekir.

FCP ve LCP değerlerini alan verisiyle izlemek için web-vitals kütüphanesi kullanışlıdır. Lab ortamında "Slow 4G" bant genişliği profiliyle çalıştırılan testler, streaming ile klasik SSR arasındaki farkı belirginleştirir; normal bağlantıda fark küçük görünebilir ama gerçek kullanıcı dağılımında yavaş bağlantılar çoğunlukla göz ardı edilen bir segment oluşturur.

Sunucu tarafında Node.js'in perf_hooks modülüyle shell gönderim zamanını loglayabilirsiniz:

const { performance } = require('perf_hooks');

const { pipe } = renderToPipeableStream(<App />, {
  onShellReady() {
    const shellTime = performance.now();
    console.log(`Shell hazır: ${shellTime.toFixed(1)} ms`);
    pipe(res);
  },
  onAllReady() {
    const totalTime = performance.now();
    console.log(`Akış tamamlandı: ${totalTime.toFixed(1)} ms`);
  }
});

Shell süresi ile toplam süre arasındaki fark, Suspense bileşenlerinin veri bekleme süresidir. Bağımsız Suspense sınırları her biri ayrı çözümleneceğinden en yavaş bileşen toplam akış süresini belirler; bunu kısaltmak doğrudan LCP'ye yansır. Hangi bileşenin ağır olduğunu görmek için bu logu üretim ortamında örneklemeli biçimde tutmak yeterlidir.

Streaming SSR, tüm SSR yaklaşımlarının yerini almaz. Basit ve hızlı sayfalar için ek karmaşıklık yaratır. Ama yavaş veri kaynaklarına bağımlı, içerik ağırlıklı ve ilk görüntüleme hızının önemli olduğu sayfalarda - ürün sayfaları, haber akışları, kişiselleştirilmiş dashboard'lar - FCP'yi belirgin biçimde öne çeker.

React 18'in bu modeli Suspense ile çerçevelemesi, bileşen düzeyinde karar vermeyi kolaylaştırdı. Önce shell'i tasarlamak, sonra Suspense sınırlarını veriye bağımlı noktalar etrafında çizmek en sağlıklı başlangıç yöntemidir. Her bileşeni ayrı ayrı değerlendirmek ve ölçümü ihmal etmemek gerekir: shell gönderim süresi ve FCP birlikte izlendiğinde streaming'in katkısı somutlaşır.

Akış mimarisi kurulduktan sonra selective hydration da devreye girer; tarayıcı görünürdeki bileşenleri önce hydrate ettiğinden sayfa tıklanabilir hale gelmesi de öne çekilir. Yavaş veriyi beklerken kullanıcının sayfanın üst kısmıyla etkileşime girebilmesi, hem hız metriklerini hem de algılanan performansı birlikte iyileştirir.