SSR Performance Landscape
Server-Side Rendering has evolved significantly. Traditional SSR renders the full page on the server, sends HTML to the client, then hydrates the entire page with JavaScript. Modern approaches like streaming SSR, React Server Components (RSC), and selective hydration dramatically improve performance by sending HTML progressively and minimizing client-side JavaScript.
SSR Rendering Strategies
- Static Generation (SSG): Pre-rendered at build time. Fastest possible TTFB. Ideal for content that rarely changes.
- Incremental Static Regeneration (ISR): Static pages that revalidate in the background. Best of both static and dynamic.
- Server-Side Rendering (SSR): Rendered on every request. Required for personalized or real-time content.
- Streaming SSR: Send HTML as it is rendered. User sees content progressively instead of waiting for the full page.
- React Server Components: Execute on the server, send serialized output. Zero client JS for server components.
Streaming SSR with React and Next.js
// app/dashboard/page.tsx — Streaming with Suspense boundaries
import { Suspense } from 'react';
// This component fetches data on the server (RSC)
async function SlowDataSection() {
const data = await fetch('https://api.example.com/slow-data', {
next: { revalidate: 60 },
});
const result = await data.json();
return <div>{result.content}</div>;
}
async function FastHeader() {
const data = await fetch('https://api.example.com/fast-data', {
cache: 'force-cache',
});
const result = await data.json();
return <header><h1>{result.title}</h1></header>;
}
// Skeleton component for loading state
function DataSkeleton() {
return (
<div className="space-y-4 animate-pulse">
<div className="h-8 bg-gray-200 rounded w-3/4" />
<div className="h-4 bg-gray-200 rounded w-full" />
<div className="h-4 bg-gray-200 rounded w-5/6" />
</div>
);
}
export default function DashboardPage() {
return (
<div>
{/* FastHeader renders immediately */}
<Suspense fallback={<div className="h-12 bg-gray-200 animate-pulse" />}>
<FastHeader />
</Suspense>
{/* SlowDataSection streams in when ready */}
<Suspense fallback={<DataSkeleton />}>
<SlowDataSection />
</Suspense>
</div>
);
}React Server Components Performance
// Server Components send ZERO JavaScript to the client
// app/blog/[slug]/page.tsx
import { notFound } from 'next/navigation';
import { getPost, getRelatedPosts } from '@/lib/blog';
import { formatDate } from '@/lib/utils';
// These imports are server-only — never sent to the client bundle
export default async function BlogPost({ params }: { params: { slug: string } }) {
const post = await getPost(params.slug);
if (!post) notFound();
const relatedPosts = await getRelatedPosts(post.id);
// All of this rendering happens on the server
// The client receives only HTML — zero JavaScript for this component
return (
<article>
<h1>{post.title}</h1>
<time>{formatDate(post.publishedAt)}</time>
<div dangerouslySetInnerHTML={{ __html: post.htmlContent }} />
<aside>
<h2>Related Posts</h2>
{relatedPosts.map(p => (
<a key={p.id} href={`/blog/${p.slug}`}>{p.title}</a>
))}
</aside>
</article>
);
}
// Only interactive parts use 'use client'
// components/LikeButton.tsx
'use client';
import { useState } from 'react';
export function LikeButton({ postId }: { postId: string }) {
const [liked, setLiked] = useState(false);
// Only THIS component's code is sent to the client
return (
<button onClick={() => setLiked(!liked)}>
{liked ? 'Liked' : 'Like'}
</button>
);
}Caching SSR Responses
// Next.js App Router caching strategies
// 1. Static (cached at build time)
// app/about/page.tsx — No dynamic functions = fully static
export default function AboutPage() {
return <div>About us</div>; // Cached indefinitely
}
// 2. ISR (revalidate on a timer)
async function getProducts() {
const res = await fetch('https://api.example.com/products', {
next: { revalidate: 3600 }, // Revalidate every hour
});
return res.json();
}
// 3. Dynamic with cache headers
// app/api/data/route.ts
import { NextResponse } from 'next/server';
export async function GET() {
const data = await fetchData();
return NextResponse.json(data, {
headers: {
'Cache-Control': 'public, s-maxage=60, stale-while-revalidate=300',
},
});
}
// 4. generateStaticParams for pre-rendering dynamic routes
export async function generateStaticParams() {
const posts = await getAllPosts();
return posts.map((post) => ({ slug: post.slug }));
// All these pages are pre-rendered at build time
}SSR Performance Best Practices
- Prefer Server Components: Keep components on the server unless they need interactivity
- Use Suspense for streaming: Wrap slow data fetches in Suspense boundaries
- Cache aggressively: Use ISR for content that changes infrequently
- Minimize client components: Push 'use client' boundaries as low as possible
- Pre-render static paths: Use generateStaticParams for known dynamic routes