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:
- Sunucu, shell (layout, navigation, skeleton) HTML'ini hemen gönderir
- Kullanıcı shell'i anında görür
- Suspense boundary'leri içindeki veriler hazır oldukça stream edilir
- 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>
);
}
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!
}
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',
};
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.