TTFB (Time to First Byte) bir sayfanın algılanan hızını doğrudan belirler. Tarayıcı ilk byte'ı alana kadar hiçbir şey yapmaz: CSS indiremez, JavaScript çözümleyemez, render döngüsünü başlatamaz. Geleneksel sunucu taraflı render bu süreyi uzatır çünkü tüm HTML üretilmeden tek bir yanıt gönderilir. Veritabanı sorgusu, harici API çağrısı veya yavaş bir bileşen birleşimi sayfanın en altındaki içerik yüklenene kadar tarayıcıyı bekleterek tutar.

React 18, bu bekleme süresini parçalara böler. renderToPipeableStream ile sunucu HTML'yi tek seferde değil, hazır olan kısımları önce göndererek akıtır. Tarayıcı ilk parçayı alır almaz <head> içeriğini işlemeye, CSS'i çekmeye, kritik fontları istek listesine almaya başlar. Kullanıcı sayfanın henüz boş olmadığını görür; sayfa çerçevesi belirir, içerik sonradan dolmaya devam eder.

Streaming SSR her projede otomatik kazanç sağlamaz. Uygulamanın nasıl yapılandırıldığı, Suspense sınırlarının nereye konulduğu ve hangi verinin ne zaman geldiği sonucu belirler. renderToPipeableStream shell'i erken gönderir; Suspense sınırı, hangi parçanın ne zaman akacağını kontrol eder.

TTFB'yi Geciktiren Sunucu Tarafı Darboğazlar

Geleneksel SSR akışı şöyle işler: istek sunucuya gelir, bileşen ağacı render edilir, veri bağımlılıkları çözülür, tam HTML string oluşturulur ve son olarak yanıt gönderilir. Buradaki sorun "tam HTML" ifadesinde saklıdır. Bir sayfada üç farklı API çağrısı varsa ve biri 800 ms sürüyorsa, diğerleri 50 ms'de bitmiş olsa bile tarayıcı 800 ms bekler.

Node.js tabanlı bir React uygulamasında renderToString kullanan yapı, sayfanın tamamını bellekte oluşturur. Büyük bileşen ağaçları hem bellek hem CPU tüketir. Tüm bu işlem tamamlanana kadar tarayıcıya tek bir byte bile gitmez. Yüksek trafikli ortamlarda bu durum event loop'u kilitleyebilir ve sonraki isteklerin sıraya girmesine yol açar.

Streaming bu darboğazı ortadan kaldırmaz; ikiye böler. Tarayıcıya önce "shell" adı verilen sayfa iskeleti gönderilir. Veritabanından gelen ürün listesi gibi geciken içerikler için yer tutucu (fallback) yerleştirilir. Veri hazır olduğunda ilgili HTML parçası aynı TCP bağlantısı üzerinden akıtılır ve tarayıcı yer tutucuyu gerçek içerikle değiştirir.

renderToPipeableStream Nasıl Çalışır?

renderToPipeableStream, React 18 ile gelen ve Node.js Writable stream'leriyle doğrudan çalışan API'dir. renderToString'den farkı sonucunu string olarak değil, akıtılabilir bir nesne olarak döndürmesidir.

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

function handleRequest(req, res) {
  const { pipe, abort } = renderToPipeableStream(<App />, {
    bootstrapScripts: ['/static/js/main.js'],
    onShellReady() {
      res.statusCode = 200;
      res.setHeader('Content-Type', 'text/html');
      pipe(res);
    },
    onShellError(error) {
      res.statusCode = 500;
      res.end('<h1>Bir hata oluştu</h1>');
    },
    onError(error) {
      console.error(error);
    }
  });
}

onShellReady geri çağrısı, shell hazır olduğunda tetiklenir; bu noktada pipe(res) çağrısı yanıtı tarayıcıya akıtmaya başlar. Shell; <html>, <head>, <body> etiketlerini ve Suspense sınırı dışındaki tüm içeriği kapsar. Suspense içindeki bileşenler hazır olduğunda ek HTML parçaları aynı bağlantı üzerinden gönderilir.

onAllReady geri çağrısı da vardır; bu, tüm render tamamlanana kadar bekler. Botlar veya önbellekleme katmanları için tam HTML gerekiyorsa onAllReady kullanılır, ancak TTFB avantajı bu durumda kaybolur. İkisi arasındaki seçim doğrudan amaca bağlıdır: canlı kullanıcılar için onShellReady, önbellek doldurmak için onAllReady.

Suspense Sınırları: Hangi Parça Ne Zaman Akar?

Streaming SSR'nin kalbi Suspense sınırlarında yatar. React, bileşen ağacındaki bir Suspense sınırına denk geldiğinde o sınırın içini shell'e dahil etmez; fallback yerleştirir ve işlemeye devam eder. Veri hazır olduğunda o parçayı akıtır.

function Page() {
  return (
    <Layout>
      <Header />
      <Suspense fallback={<ProductListSkeleton />}>
        <ProductList />
      </Suspense>
      <Suspense fallback={<ReviewSkeleton />}>
        <ReviewSection />
      </Suspense>
      <Footer />
    </Layout>
  );
}

Burada Header, Layout ve Footer hemen shell'e girer. ProductList ve ReviewSection kendi Suspense sınırları içinde bekler. İkisi bağımsız beklediğinden hangisi daha erken hazırsa o önce akar; ReviewSection 200 ms'de hazırsa ve ProductList hâlâ bekliyorsa tarayıcı yorumları önce alır.

Sınırın yeri kritik karardır. Çok geniş bir Suspense sayfanın büyük bölümünü geciktirir. Çok dar Suspense sınırları bileşen ağacını karmaşık hâle getirir ve zaman zaman gereksiz layout shift üretir. Bir ürün detay sayfasında başlık, fiyat ve ana görseli shell'de tutmak; kullanıcı yorumlarını ve "benzer ürünler" bölümünü Suspense içine almak makul bir ayrımdır.

Shell Tasarımı: Önce Ne Gitmeli?

Shell, tarayıcının ilk aldığı parçadır. Ne kadar zengin olursa TTFB'nin somut faydası o kadar büyür. İdeal bir shell şunları içerir: sayfa çatısını oluşturan HTML iskelet, <head> içindeki CSS linkleri, kritik fontların preload direktifleri ve navigasyon gibi her sayfada aynı olan statik parçalar.

Shell mümkün olduğunca az veri bağımlılığı taşımalıdır. Navigasyonda kullanıcının adı görünüyorsa ve bu ad bir API çağrısı gerektiriyorsa, o çağrı shell'i geciktirir. Seçenekler ikidir: navigasyonu da bir Suspense sınırına almak ya da kullanıcı adını ayrı bir istemci taraflı sorguda yüklemek. Sayfanın genel yapısına bakılarak hangisi daha doğru olduğuna karar verilir.

Veri bağımlı olmayan her şey shell'de olmalıdır. Uygulamada bazen bileşenler veriyi kendi içinde çeker ve shell hesaplaması sırasında bu bağımlılıklar görünmez olur. use hook veya React Server Components ile veri çekme düzeni kurulurken her bileşenin hangi katmanda veri talep ettiği takip edilmelidir; aksi hâlde shell'in neden geciktiği anlaşılamaz.

Paralel Veri Çekme ile Gecikmeyi Azaltmak

Streaming SSR TTFB'yi öne çeker; geciken içeriğin kullanıcıya ulaşma süresi ise veri çekme düzenine bağlıdır. Suspense sınırı içindeki bileşenler render sırasında veri çekiyorsa waterfall riski doğar: her bileşen, üst bileşen render edilene kadar kendi veri isteğini başlatamaz.

Waterfall'ı kırmanın yolu veri çekme işlemini render ağacının mümkün olduğunca üst kısmında başlatmaktır. Next.js App Router gibi meta-framework'lerde generateMetadata ve layout server component'leri bu iş için kullanılır. Doğrudan renderToPipeableStream kullanılıyorsa, istek işleyicisinde tüm veri bağımlılıklarını aynı anda başlatmak ve bunları bileşenlere prop olarak geçmek sağlıklı bir yaklaşımdır.

async function handleRequest(req, res) {
  // Tüm veri isteklerini paralel başlat
  const productPromise = fetchProduct(req.params.id);
  const reviewsPromise = fetchReviews(req.params.id);

  const { pipe } = renderToPipeableStream(
    <App productPromise={productPromise} reviewsPromise={reviewsPromise} />,
    { onShellReady() { pipe(res); } }
  );
}

Shell tarayıcıya giderken arka planda productPromise ve reviewsPromise zaten çalışıyor olur. React, Suspense sınırı içindeki bileşen o promise'i okuduğunda veri hazırsa anında render eder; hazır değilse promise çözülene kadar bekler. Bu düzende toplam gecikme en uzun tek sorgunun süresine indirgenmiş olur; sorgular sırayla değil paralel işlenir.

Ne Zaman Streaming SSR Uygun Değildir?

Streaming her durumda kazandırmaz. Yanıtın tamamı CDN kenar önbelleğine alınıyorsa streaming anlamsızlaşır. CDN tam HTML bekler; akıtılan yanıtı önbelleğe almak için ek yapılandırma gerekir ve her altyapıda mümkün olmaz. Statik veya yarı statik sayfalarda önceden üretilmiş HTML çok daha az karmaşıklıkla aynı TTFB değerini sunar.

Suspense'e alınacak içerik yoksa fayda küçülür. Sayfanın tamamı tek bir veritabanı sorgusundan besleniyor ve tüm içerik birbirine bağlıysa Suspense sınırı çizmek için yer kalmaz; bu durumda shell hemen hemen boş olur ve akıtmanın pratikte etkisi olmaz. Yalnızca birkaç kilobyte'lık bir veri nedeniyle streaming altyapısı kurmak ölçülü bir karar değildir.

Eski proxy ve CDN yapılandırmaları zaman zaman akıtılan yanıtları tamponlar; bu durumda tarayıcı her şey tamamlanana kadar hiçbir şey almaz ve streaming avantajı yok olur. Transfer-Encoding: chunked başlığının proxy zinciri boyunca korunup korunmadığı doğrulanmalıdır. Bu başlık zincirde bir noktada kaldırılıyorsa sorun sunucu kodunda değil altyapı yapılandırmasındadır.

Ölçmek: Hangi Metriklere Bakılır?

Streaming SSR kurulumu tamamlandığında ölçüm olmadan başarı değerlendirilemez. Temel dört metrik birlikte okunmalıdır.

TTFB: Sunucunun ilk byte'ı gönderdiği an. Streaming ile bu değerin düştüğü görülür çünkü shell oluşturmak tüm sayfayı oluşturmaktan çok daha hızlıdır. Chrome DevTools Network sekmesinde "Waiting (TTFB)" satırı bu değeri gösterir.

FCP (First Contentful Paint): Tarayıcının ekrana ilk anlamlı içeriği çizdiği an. Shell CSS ve işaretlemesini taşıdığı için FCP da öne gelir. Ancak shell içeriksizse, yalnızca boş iskeletten oluşuyorsa, FCP erken gelse de kullanıcı deneyimi iyileşmez.

LCP (Largest Contentful Paint): Sayfanın en büyük görsel öğesinin ekrana geldiği an. LCP bileşeni Suspense içindeyse, örneğin ürün görseli bir API çağrısıyla geliyorsa, LCP geç kalabilir. Streaming bunu otomatik çözmez; kritik görsel içeriğin shell'de ya da mümkün olan en hızlı Suspense sınırında olması gerekir.

INP (Interaction to Next Paint): Streaming, INP üzerinde doğrudan etki etmez. JavaScript'in daha erken gelmeye başlaması hidrasyonun daha hızlı tamamlanmasına yol açar ve bu durum dolaylı olarak INP'yi destekler; etkinin büyüklüğü uygulamanın hidrasyon ağırlığına bağlıdır.

Streaming SSR, React 18'in getirdiği değişikliklerin en somut faydalarından birini taşır: sunucudaki gecikmeyi kullanıcıdan gizlemek yerine kullanıcıyla paralel işlemek. Tarayıcı hazır olan parçaları işlerken sunucu kalan parçaları üretmeye devam eder ve bağlantı açık kaldığı sürece içerik akmayı sürdürür.

Uygulamada en büyük zorluk Suspense sınırlarını doğru konumlandırmaktır. Çok erken konan sınır layout shift üretir; çok geç konan sınır streaming'in faydalarını törpüler. Sayfanın hangi parçasının hangi veriye bağımlı olduğunu ve bu verinin ne kadar sürede geldiğini bilmeden yerleştirilen Suspense sınırları gelişigüzel kalmaya mahkumdur.

renderToPipeableStream Node.js ortamında doğrudan kullanılabilir; Next.js App Router, Remix ve benzeri meta-framework'ler bu API'nin üzerine soyutlamalar kurar. Mevcut altyapı bu framework'lerden birindeyse streaming çoğunlukla yapılandırmayla etkinleştirilir ve elle kod yazılmasına gerek kalmaz. Altta ne döndüğünü anlamak, özellikle performans sorunlarını teşhis ederken, doğru müdahaleyi bulmayı kolaylaştırır.