Performance --- Caching, Suspense & Streaming
⏱ 18 min read read
Next.js Caching Layers (from fastest to slowest):
1. Memory Cache --- per-request dedup (same fetch URL called twice →
one request)
2. Data Cache --- persistent across requests, stored on server
(force-cache)
3. Full Route Cache --- statically rendered pages cached on disk
(SSG/ISR)
4. Router Cache --- client-side, caches visited pages in browser
memory
loading.tsx --- Instant Loading UI:
Create loading.tsx next to page.tsx. It shows instantly while the page
fetches, using Suspense under the hood.
// app/dashboard/loading.tsx
export default function DashboardLoading() {
return (
<div className="animate-pulse">
<div className="h-8 bg-gray-200 rounded w-48 mb-4" />
<div className="h-4 bg-gray-200 rounded w-full mb-2" />
<div className="h-4 bg-gray-200 rounded w-3/4" />
</div>
);
}
Suspense --- Stream Individual Components:
import { Suspense } from 'react';
export default function Page() {
return (
<div>
<h1>Dashboard</h1> {/* renders instantly */}
<Suspense fallback={<StatsSkeletion />}>
<Stats /> {/* streams in when ready */}
</Suspense>
<Suspense fallback={<TableSkeleton />}>
<StudentsTable /> {/* streams in independently */}
</Suspense>
</div>
);
}
On-Demand Revalidation:
// Revalidate by path
import { revalidatePath } from 'next/cache';
revalidatePath('/students');
// Revalidate by tag
import { revalidateTag } from 'next/cache';
revalidateTag('students');
// Tag a fetch
fetch(url, { next: { tags: ['students'] } })
Use Suspense boundaries to parallelize slow fetches and stream
results.
Skeleton UIs (loading.tsx) make pages feel faster than spinners.
unstable_cache() wraps any async function (not just fetch) with
caching.
Server Actions automatically call revalidatePath/revalidateTag after
mutations.
1. Memory Cache --- per-request dedup (same fetch URL called twice →
one request)
2. Data Cache --- persistent across requests, stored on server
(force-cache)
3. Full Route Cache --- statically rendered pages cached on disk
(SSG/ISR)
4. Router Cache --- client-side, caches visited pages in browser
memory
loading.tsx --- Instant Loading UI:
Create loading.tsx next to page.tsx. It shows instantly while the page
fetches, using Suspense under the hood.
// app/dashboard/loading.tsx
export default function DashboardLoading() {
return (
<div className="animate-pulse">
<div className="h-8 bg-gray-200 rounded w-48 mb-4" />
<div className="h-4 bg-gray-200 rounded w-full mb-2" />
<div className="h-4 bg-gray-200 rounded w-3/4" />
</div>
);
}
Suspense --- Stream Individual Components:
import { Suspense } from 'react';
export default function Page() {
return (
<div>
<h1>Dashboard</h1> {/* renders instantly */}
<Suspense fallback={<StatsSkeletion />}>
<Stats /> {/* streams in when ready */}
</Suspense>
<Suspense fallback={<TableSkeleton />}>
<StudentsTable /> {/* streams in independently */}
</Suspense>
</div>
);
}
On-Demand Revalidation:
// Revalidate by path
import { revalidatePath } from 'next/cache';
revalidatePath('/students');
// Revalidate by tag
import { revalidateTag } from 'next/cache';
revalidateTag('students');
// Tag a fetch
fetch(url, { next: { tags: ['students'] } })
Use Suspense boundaries to parallelize slow fetches and stream
results.
Skeleton UIs (loading.tsx) make pages feel faster than spinners.
unstable_cache() wraps any async function (not just fetch) with
caching.
Server Actions automatically call revalidatePath/revalidateTag after
mutations.
Log in to track your progress and earn badges as you complete lessons.
Log In to Track Progress