Next.js 15 marks a foundational shift in how full-stack React web applications are built, rendered, and delivered. The transition from legacy Pages router (getServerSideProps) to the App Router represents a paradigm leap into React Server Components (RSC), streaming SSR, and Partial Prerendering (PPR).
In this deep dive, we break down how Next.js 15 delivers zero-bundle-size server components and ultra-responsive user experiences.
Server components execute exclusively on the node/edge runtime, sending only streaming HTML and RSC payloads.
RSC vs Client Component Execution Model
| Characteristic | React Server Component (Default) | Client Component ("use client") |
|---|---|---|
| Execution Environment | Server-only (Node.js / Edge) | Server SSR + Client Hydration |
| JavaScript Bundle Size | 0 KB sent to browser | Shipped in client JS bundle |
| Access to DB & Secrets | Direct access (pg, sql, API tokens) | Strictly forbidden (exposed to browser) |
Interactivity (onClick, useState) | Not supported | Fully supported |
| Data Fetching | Direct async/await in component | React 19 use(), SWR, or useEffect |
Streaming SSR with Suspense Boundaries
In traditional SSR, the server must wait for all database queries to complete before sending a single byte of HTML. If one slow microservice takes 1.5s, the user stares at a white blank screen for 1.5s.
Streaming SSR solves this by flushing the initial static HTML shell instantly, streaming dynamic chunks as asynchronous promises resolve:
import { Suspense } from "react";
import { getProfile, getProjects } from "@/lib/api";
// Fast Server Component
export default async function DashboardPage() {
const profile = await getProfile();
return (
<div className="max-w-5xl mx-auto py-12 px-6">
<header className="mb-8">
<h1 className="text-3xl font-bold">Welcome back, {profile.full_name}</h1>
</header>
{/* Stream the slow analytics component without delaying header render */}
<Suspense fallback={<AnalyticsSkeleton />}>
<SlowAnalyticsWidget />
</Suspense>
</div>
);
}
// Slow async component wrapped in Suspense
async function SlowAnalyticsWidget() {
const projects = await getProjects(); // Takes 800ms
return (
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
{projects.map((p) => (
<div key={p.id} className="p-6 rounded-xl border border-line bg-card">
<h3 className="font-semibold">{p.name}</h3>
<p className="text-sm text-muted">{p.category}</p>
</div>
))}
</div>
);
}
function AnalyticsSkeleton() {
return <div className="h-48 rounded-xl bg-ink/[0.04] animate-pulse" />;
}What is Partial Prerendering (PPR)?
Next.js 15 introduces Partial Prerendering (PPR), bridging static site generation and dynamic streaming:
- Static Shell: At build time, Next.js prerenders the static layout and skeleton frames into pure static HTML hosted on the Edge CDN.
- Dynamic Holes: When a user visits the URL, the CDN serves the static shell in 10ms, and the serverless runtime concurrently streams dynamic content into the Suspense boundaries over the same HTTP stream.
Production Recommendation: Place "use client" boundaries as deep down the component tree as possible (e.g., on individual buttons or inputs) so parent layouts remain 100% server components with zero client JavaScript overhead.