İçeriğe geç
nextjs

Next.js Streaming SSR ve Progressive Rendering

Tarık Tunç
Next.js Streaming SSR ve Progressive Rendering

Next.js Streaming SSR ve Progressive Rendering

Next.js App Router, React 18'in Streaming SSR özelliğini yerleşik olarak destekler. Sayfanın hazır olan bölümlerini anında göndererek, yavaş veri kaynaklarının tüm sayfayı engellemesini önler. Progressive rendering ile kullanıcılar içeriği aşamalı olarak görür ve daha hızlı bir deneyim yaşar. Bu yazıda Next.js'te streaming SSR'ın nasıl çalıştığını ve nasıl optimize edileceğini adım adım inceleyeceğiz.

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

Geleneksel SSR'da tüm veri fetch'leri tamamlanmadan hiçbir HTML gönderilmez. Streaming SSR'da ise sayfa parça parça (chunk) gönderilir:

  1. Sunucu, shell (layout, navigation, skeleton) HTML'ini hemen gönderir
  2. Kullanıcı shell'i anında görür
  3. Suspense boundary'leri içindeki veriler hazır oldukça stream edilir
  4. Her parça geldiğinde DOM otomatik güncellenir
// app/dashboard/page.js
import { Suspense } from 'react';
import { RevenueChart } from '@/components/RevenueChart';
import { LatestOrders } from '@/components/LatestOrders';
import { TopProducts } from '@/components/TopProducts';
import { DashboardSkeleton } from '@/components/Skeletons';

export default function DashboardPage() {
  return (
    <div className="dashboard">
      <h1>Dashboard</h1>

      {/* Her bölüm bağımsız olarak stream edilir */}
      <div className="grid grid-cols-2 gap-4">
        <Suspense fallback={<ChartSkeleton />}>
          <RevenueChart />
        </Suspense>

        <Suspense fallback={<OrdersSkeleton />}>
          <LatestOrders />
        </Suspense>
      </div>

      <Suspense fallback={<ProductsSkeleton />}>
        <TopProducts />
      </Suspense>
    </div>
  );
}

loading.js ile Instant Loading States

Next.js App Router'da loading.js dosyası, route segment için otomatik bir Suspense boundary oluşturur:

// app/dashboard/loading.js
export default function DashboardLoading() {
  return (
    <div className="dashboard-skeleton">
      <div className="skeleton-header animate-pulse" />
      <div className="grid grid-cols-2 gap-4">
        <div className="skeleton-card animate-pulse" />
        <div className="skeleton-card animate-pulse" />
      </div>
      <div className="skeleton-table animate-pulse" />
    </div>
  );
}

// Bu, aşağıdakine eşdeğerdir:
// <Suspense fallback={<DashboardLoading />}>
//   <DashboardPage />
// </Suspense>

Async Server Components ile Streaming

Server Component'ler async olabilir ve doğrudan veri fetch'i yapabilir. Suspense boundary'leri içine yerleştirildiğinde otomatik olarak stream edilir:

// components/RevenueChart.jsx (Server Component)
async function RevenueChart() {
  // Bu fetch 2-3 saniye sürebilir
  const revenue = await fetch('https://api.example.com/revenue', {
    next: { revalidate: 3600 },
  }).then((res) => res.json());

  return (
    <div className="chart-container">
      <h2>Gelir Grafiği</h2>
      <div className="chart">
        {revenue.months.map((month) => (
          <div
            key={month.name}
            className="bar"
            style={{ height: `${month.value / 100}%` }}
          >
            <span>{month.name}</span>
          </div>
        ))}
      </div>
    </div>
  );
}

// components/LatestOrders.jsx (Server Component)
async function LatestOrders() {
  // Bu fetch 1 saniye sürebilir
  const orders = await fetch('https://api.example.com/orders/latest').then(
    (res) => res.json()
  );

  return (
    <div className="orders-list">
      <h2>Son Siparişler</h2>
      <ul>
        {orders.map((order) => (
          <li key={order.id}>
            <span>{order.customer}</span>
            <span>{order.total} TL</span>
          </li>
        ))}
      </ul>
    </div>
  );
}

Nested Suspense ile Granüler Streaming

export default function ProductPage({ params }) {
  return (
    <div>
      {/* Ürün bilgisi hızlı yüklenir */}
      <Suspense fallback={<ProductSkeleton />}>
        <ProductDetails id={params.id} />
      </Suspense>

      {/* Yorumlar daha yavaş yüklenebilir */}
      <Suspense fallback={<ReviewsSkeleton />}>
        <ProductReviews id={params.id} />

        {/* Nested: Yorum istatistikleri en son yüklenebilir */}
        <Suspense fallback={<StatsSkeleton />}>
          <ReviewStats id={params.id} />
        </Suspense>
      </Suspense>

      {/* Öneriler bağımsız stream edilir */}
      <Suspense fallback={<RecommendationsSkeleton />}>
        <Recommendations productId={params.id} />
      </Suspense>
    </div>
  );
}
Bilgi: Suspense boundary'leri bağımsız olarak resolve olur. Yukarıdaki örnekte Reviews 3 saniyede, ProductDetails 1 saniyede ve Recommendations 2 saniyede hazır olabilir. Her biri hazır olduğu anda kullanıcıya gösterilir.

Skeleton Bileşenleri Best Practices

// components/Skeletons.jsx
export function CardSkeleton() {
  return (
    <div className="card animate-pulse">
      <div className="h-4 bg-gray-200 rounded w-3/4 mb-4" />
      <div className="h-3 bg-gray-200 rounded w-full mb-2" />
      <div className="h-3 bg-gray-200 rounded w-5/6 mb-2" />
      <div className="h-3 bg-gray-200 rounded w-4/6" />
    </div>
  );
}

export function TableSkeleton({ rows = 5 }) {
  return (
    <div className="table-skeleton">
      {Array.from({ length: rows }).map((_, i) => (
        <div key={i} className="flex gap-4 py-3 animate-pulse">
          <div className="h-4 bg-gray-200 rounded w-1/4" />
          <div className="h-4 bg-gray-200 rounded w-1/3" />
          <div className="h-4 bg-gray-200 rounded w-1/6" />
        </div>
      ))}
    </div>
  );
}

// Skeleton, gerçek içerikle aynı boyutta olmalı — layout shift'i önler
export function ChartSkeleton() {
  return (
    <div className="chart-container" style={{ height: 400 }}>
      <div className="h-6 bg-gray-200 rounded w-1/3 mb-4 animate-pulse" />
      <div className="h-full bg-gray-100 rounded animate-pulse" />
    </div>
  );
}

Parallel Data Fetching

// Kötü: Sıralı fetch (waterfall)
async function SlowDashboard() {
  const revenue = await getRevenue();     // 2s
  const orders = await getOrders();       // 1s
  const products = await getProducts();   // 1.5s
  // Toplam: ~4.5s

  return <div>{/* render */}</div>;
}

// İyi: Paralel fetch
async function FastDashboard() {
  const [revenue, orders, products] = await Promise.all([
    getRevenue(),     // 2s
    getOrders(),      // 1s (paralel)
    getProducts(),    // 1.5s (paralel)
  ]);
  // Toplam: ~2s (en yavaş olanın süresi)

  return <div>{/* render */}</div>;
}

// En iyi: Suspense ile bağımsız streaming
function OptimalDashboard() {
  return (
    <div>
      <Suspense fallback={<Skeleton />}>
        <Revenue />       {/* 2s sonra görünür */}
      </Suspense>
      <Suspense fallback={<Skeleton />}>
        <Orders />        {/* 1s sonra görünür */}
      </Suspense>
      <Suspense fallback={<Skeleton />}>
        <Products />      {/* 1.5s sonra görünür */}
      </Suspense>
    </div>
  );
  // Her bölüm hazır olduğu anda gösterilir!
}
İpucu: Streaming'in en büyük avantajı, yavaş veri kaynaklarının hızlı bölümleri engellemesidir. Kritik içeriği (ürün adı, fiyat) Suspense dışında tutarak anında gösterin, daha az kritik içeriği (yorumlar, öneriler) Suspense ile sarmalayarak aşamalı yükleyin.

Streaming ve SEO

Next.js, arama motoru bot'ları için özel davranış uygular:

// Next.js, bot'lar için tüm Suspense boundary'lerini bekler
// ve tam HTML'i gönderir. Normal kullanıcılar için streaming yapar.

// next.config.js ile bot detection yapılandırması
// Next.js bunu otomatik olarak yönetir

// Metadata her zaman shell'de gönderilir
export const metadata = {
  title: 'Dashboard',
  description: 'Yönetim paneli',
};
Uyarı: Streaming, CDN ve reverse proxy yapılandırmasını etkileyebilir. Bazı CDN'ler chunked response'u buffer'lar. Vercel bu sorunu otomatik çözer, ancak self-hosted ortamlarda proxy ayarlarını kontrol edin.

Performans Metrikleri Etkisi

Metrik Streaming Olmadan Streaming ile
TTFB Tüm veri bekler (yavaş) Shell anında gönderilir (hızlı)
FCP Geç Shell ile birlikte (erken)
LCP En yavaş fetch'e bağlı Kritik içeriğin hızına bağlı
CLS Düşük (tek seferde render) Skeleton tasarımına bağlı

Sonuç

Next.js Streaming SSR, kullanıcı deneyimini önemli ölçüde iyileştiren güçlü bir özelliktir. Suspense boundary'leri ile sayfanın hangi bölümlerinin aşamalı yükleneceğini kontrol edebilir, loading.js ile otomatik skeleton state'leri oluşturabilir ve nested Suspense ile granüler streaming yapabilirsiniz. Skeleton bileşenlerini gerçek içerikle aynı boyutta tasarlayarak CLS'yi minimize edin, kritik içeriği Suspense dışında tutarak FCP'yi iyileştirin. Next.js, SEO bot'ları için tam HTML'i otomatik gönderdiğinden, streaming kullanmak SEO'yu olumsuz etkilemez.

Kaynaklar

İlgili Yazılar