İçeriğe geç
react

React Streaming SSR: Aşamalı Render

Tarık Tunç
React Streaming SSR: Aşamalı Render

React Streaming SSR: Aşamalı Render

Geleneksel Server-Side Rendering (SSR), sayfanın tamamını sunucuda render edip tek seferde istemciye gönderir. Bu yaklaşımda yavaş veri kaynakları tüm sayfanın yüklenmesini geciktirebilir. React Streaming SSR, sayfayı parça parça göndererek kullanıcıya daha hızlı bir ilk görüntüleme deneyimi sunar. Bu yazıda Streaming SSR'ın çalışma prensibini, Suspense entegrasyonunu ve pratik uygulama yöntemlerini inceleyeceğiz.

Geleneksel SSR'ın Sorunları

Klasik SSR sürecinde dört adım sıralı olarak gerçekleşir:

  1. Sunucuda tüm veriler fetch edilir (waterfall)
  2. Sunucuda tüm HTML render edilir
  3. HTML ve JavaScript istemciye gönderilir
  4. Hydration ile sayfa interaktif hale gelir

Bu süreçte en yavaş veri kaynağı, tüm sayfanın Time to First Byte (TTFB) değerini belirler. Streaming SSR bu sorunu çözer.

Streaming SSR Nasıl Çalışır?

Streaming SSR, HTTP chunked transfer encoding kullanarak HTML'i parça parça gönderir. React 18 ile gelen renderToPipeableStream API'si bu işlevi sağlar.

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

const app = express();

app.get('/', (req, res) => {
  const { pipe, abort } = renderToPipeableStream(
    <App />,
    {
      bootstrapScripts: ['/static/client.js'],

      onShellReady() {
        // Shell (Suspense boundary dışındaki kısım) hazır
        res.setHeader('Content-Type', 'text/html');
        pipe(res);
      },

      onShellError(error) {
        // Shell render edilemedi — fallback gönder
        res.statusCode = 500;
        res.send('<!DOCTYPE html><html><body><p>Bir hata oluştu</p></body></html>');
      },

      onAllReady() {
        // Tüm içerik (Suspense dahil) hazır
        // Crawler/bot'lar için tüm içeriği beklemek isteyebilirsiniz
      },

      onError(error) {
        console.error('SSR Hatası:', error);
      },
    }
  );

  // Timeout — 10 saniye sonra abort et
  setTimeout(() => abort(), 10000);
});

Suspense ile Streaming

Streaming SSR'ın gücü, Suspense bileşenleriyle birleştiğinde ortaya çıkar. Suspense boundary'leri, hangi bölümlerin streaming ile gönderileceğini belirler.

function App() {
  return (
    <html>
      <head><title>Uygulamam</title></head>
      <body>
        <Header />  {/* Anında gönderilir */}
        <Navigation />  {/* Anında gönderilir */}

        <main>
          <Suspense fallback={<HeroSkeleton />}>
            <HeroSection />  {/* Veri hazır olunca stream edilir */}
          </Suspense>

          <Suspense fallback={<ProductsSkeleton />}>
            <ProductList />  {/* Bağımsız olarak stream edilir */}
          </Suspense>

          <Suspense fallback={<ReviewsSkeleton />}>
            <Reviews />  {/* En son yüklenebilir */}
          </Suspense>
        </main>

        <Footer />  {/* Anında gönderilir */}
      </body>
    </html>
  );
}
Bilgi: Streaming sırasında React, önce Suspense fallback'lerini içeren shell'i gönderir. Ardından her Suspense boundary'deki veri hazır oldukça, gerçek içeriği inline <script> tag'leri aracılığıyla gönderir. İstemci tarafındaki React, bu parçaları otomatik olarak DOM'a yerleştirir.

Shell ve Content Kavramları

Streaming SSR'da iki önemli kavram vardır:

  • Shell: Suspense boundary'leri dışındaki, anında render edilen bölüm. Header, navigation, layout yapısı ve skeleton'lar shell'in parçasıdır.
  • Content: Suspense boundary'leri içindeki, asenkron veri gerektiren bölümler. Bunlar hazır oldukça stream edilir.
// Shell: Anında gönderilen yapı
// <html>
//   <head>...</head>
//   <body>
//     <header>Logo | Nav</header>
//     <main>
//       <!-- Suspense fallback -->
//       <div class="skeleton">Yükleniyor...</div>
//     </main>
//     <footer>...</footer>
//   </body>
// </html>

// Daha sonra stream edilen içerik (inline script ile)
// <script>
//   $RC("B:0", "S:0")  // Suspense boundary'yi gerçek içerikle değiştir
// </script>

Selective Hydration

React 18'in bir diğer önemli özelliği Selective Hydration'dır. Streaming ile birlikte çalışarak, sayfanın farklı bölümlerini bağımsız olarak hydrate eder.

// Kullanıcı henüz hydrate olmamış bir bölüme tıklarsa,
// React o bölümü öncelikli olarak hydrate eder.
function App() {
  return (
    <Layout>
      <Suspense fallback={<NavSkeleton />}>
        <NavBar />  {/* Öncelikli hydrate */}
      </Suspense>

      <Suspense fallback={<SidebarSkeleton />}>
        <Sidebar />  {/* Bağımsız hydrate */}
      </Suspense>

      <Suspense fallback={<ContentSkeleton />}>
        <MainContent />  {/* Bağımsız hydrate */}
      </Suspense>

      <Suspense fallback={<CommentsSkeleton />}>
        <Comments />  {/* Son hydrate */}
      </Suspense>
    </Layout>
  );
}

Node.js Streams ile Entegrasyon

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

app.get('/', (req, res) => {
  // Özel transform stream ile HTML'i modifiye edebilirsiniz
  const transformStream = new Transform({
    transform(chunk, encoding, callback) {
      // HTML chunk'ını manipüle et
      const html = chunk.toString();
      // Örn: CSP nonce ekle
      const modified = html.replace(
        /<script/g,
        '<script nonce="abc123"'
      );
      callback(null, modified);
    },
  });

  const { pipe } = renderToPipeableStream(<App />, {
    bootstrapScripts: ['/client.js'],
    onShellReady() {
      res.setHeader('Content-Type', 'text/html; charset=utf-8');
      pipe(transformStream).pipe(res);
    },
  });
});

Web Streams API (Edge Runtime)

Edge runtime ortamlarında (Cloudflare Workers, Vercel Edge) Web Streams API kullanılır:

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

export default async function handler(request) {
  const stream = await renderToReadableStream(
    <App />,
    {
      bootstrapScripts: ['/client.js'],
      signal: AbortSignal.timeout(10000),
    }
  );

  // Bot'lar için tüm içeriği bekle
  const userAgent = request.headers.get('User-Agent') || '';
  const isBot = /bot|crawl|spider/i.test(userAgent);

  if (isBot) {
    await stream.allReady;
  }

  return new Response(stream, {
    headers: { 'Content-Type': 'text/html; charset=utf-8' },
  });
}
İpucu: SEO açısından, arama motoru bot'ları için onAllReady (Node.js) veya stream.allReady (Web Streams) kullanarak tüm içeriğin hazır olmasını beklemek iyi bir pratiktir. Normal kullanıcılar için ise streaming ile daha hızlı bir deneyim sunabilirsiniz.

Performans Karşılaştırması

Metrik Geleneksel SSR Streaming SSR
TTFB Yavaş (tüm veri beklenir) Hızlı (shell anında gönderilir)
FCP Geç (HTML tamamlandıktan sonra) Erken (shell ile birlikte)
TTI Geç (tüm sayfa hydrate) Aşamalı (selective hydration)
LCP En yavaş veri kaynağına bağlı Kritik içeriğin hızına bağlı
Uyarı: Streaming SSR, reverse proxy ve CDN yapılandırmasını etkileyebilir. Bazı proxy'ler chunked response'u buffer'layabilir. Nginx'te proxy_buffering off veya X-Accel-Buffering: no header'ı gerekebilir.

Hata Yönetimi

const { pipe } = renderToPipeableStream(<App />, {
  onShellReady() {
    // Shell başarıyla render edildi
    res.statusCode = 200;
    pipe(res);
  },
  onShellError(error) {
    // Shell render edilemedi — statik hata sayfası gönder
    res.statusCode = 500;
    res.send(fallbackHTML);
  },
  onError(error) {
    // Suspense boundary içindeki hata
    // Error Boundary fallback'i gösterilir
    logError(error);
  },
});

Sonuç

React Streaming SSR, modern web uygulamalarında performansı önemli ölçüde iyileştiren bir tekniktir. Suspense boundary'leri ile sayfanın hangi bölümlerinin streaming ile gönderileceğini kontrol edebilir, selective hydration ile kullanıcı etkileşimlerine öncelik verebilirsiniz. TTFB ve FCP metriklerinde belirgin iyileşmeler sağlar. Next.js App Router bu teknolojileri yerleşik olarak desteklediğinden, framework kullanıyorsanız ek yapılandırmaya gerek kalmadan streaming SSR'dan faydalanabilirsiniz.

Kaynaklar

İlgili Yazılar