Beyond basic parallel and sequential fetching, Next.js provides powerful patterns for optimizing data fetching: streaming with Suspense to show content progressively, automatic request deduplication to eliminate redundant fetches, preloading to start fetching early, and more. These patterns help you build applications that feel incredibly fast by showing users content as soon as possible and optimizing every data request. Let's explore these advanced strategies!
Streaming with Suspense
Streaming allows you to show fast content immediately while slow content loads in the background.
The Problem: Slow Pages
// ❌ BAD: Everything waits for slowest component
async function DashboardPage() {
const quickStats = await getQuickStats(); // 200ms
const userInfo = await getUserInfo(); // 300ms
const slowAnalytics = await getAnalytics(); // 5000ms ⏱️
return (
<div>
<QuickStats data={quickStats} />
<UserInfo data={userInfo} />
<Analytics data={slowAnalytics} />
</div>
);
}
// ⏱️ User waits 5000ms to see ANYTHING
// ❌ Quick content blocked by slow content✅ Solution: Stream with Suspense
import { Suspense } from 'react';
// Fast components - render immediately
async function QuickStats() {
const stats = await fetch('https://api.example.com/quick-stats', {
cache: 'force-cache', // Very fast from cache
}).then(r => r.json());
return (
<div className="grid grid-cols-4 gap-4">
<StatCard title="Users" value={stats.users} />
<StatCard title="Revenue" value={`$${stats.revenue}`} />
<StatCard title="Orders" value={stats.orders} />
<StatCard title="Growth" value={`${stats.growth}%`} />
</div>
);
}
async function UserInfo() {
const user = await fetch('https://api.example.com/user')
.then(r => r.json());
return (
<div className="flex items-center gap-4">
<img src={user.avatar} alt={user.name} className="w-16 h-16 rounded-full" />
<div>
<h2 className="text-xl font-bold">{user.name}</h2>
<p className="text-gray-600">{user.email}</p>
</div>
</div>
);
}
// Slow component - streams in when ready
async function Analytics() {
const analytics = await fetch('https://api.example.com/analytics', {
cache: 'no-store', // Always fresh, takes time
}).then(r => r.json());
return (
<div className="space-y-4">
<h3 className="text-2xl font-bold">Analytics Dashboard</h3>
<ComplexChart data={analytics.chartData} />
<DetailedTable data={analytics.tableData} />
</div>
);
}
// Loading fallback - shows while Analytics loads
function AnalyticsSkeleton() {
return (
<div className="space-y-4 animate-pulse">
<div className="h-8 bg-gray-200 rounded w-1/3" />
<div className="h-64 bg-gray-200 rounded" />
<div className="h-96 bg-gray-200 rounded" />
</div>
);
}
// Page composition
export default function DashboardPage() {
return (
<div className="container mx-auto px-4 py-8">
<h1 className="text-3xl font-bold mb-8">Dashboard</h1>
{/* Renders immediately - no waiting */}
<QuickStats />
<div className="my-8">
<UserInfo />
</div>
{/* Streams in when ready - doesn't block page */}
<Suspense fallback={<AnalyticsSkeleton />}>
<Analytics />
</Suspense>
</div>
);
}
// ✅ QuickStats + UserInfo show in 300ms
// ✅ Page is interactive immediately
// ✅ Analytics streams in after 5000ms
// ✅ Much better user experience!Multiple Suspense Boundaries
import { Suspense } from 'react';
// Fast: Article content
async function ArticleContent({ slug }: { slug: string }) {
const article = await fetch(`https://api.example.com/articles/${slug}`)
.then(r => r.json());
return (
<article className="prose prose-lg">
<h1>{article.title}</h1>
<div dangerouslySetInnerHTML={{ __html: article.content }} />
</article>
);
}
// Slow: Comments
async function Comments({ slug }: { slug: string }) {
const comments = await fetch(`https://api.example.com/articles/${slug}/comments`, {
cache: 'no-store',
}).then(r => r.json());
return (
<div className="space-y-4">
<h2 className="text-2xl font-bold">Comments ({comments.length})</h2>
{comments.map(comment => (
<div key={comment.id} className="border rounded p-4">
<p className="font-semibold">{comment.author}</p>
<p className="text-gray-700">{comment.text}</p>
</div>
))}
</div>
);
}
// Slow: Related articles
async function RelatedArticles({ slug }: { slug: string }) {
const related = await fetch(`https://api.example.com/articles/${slug}/related`)
.then(r => r.json());
return (
<div>
<h2 className="text-2xl font-bold mb-4">Related Articles</h2>
<div className="grid grid-cols-3 gap-4">
{related.map(article => (
<a
key={article.id}
href={`/blog/${article.slug}`}
className="border rounded p-4 hover:shadow-lg"
>
<h3 className="font-semibold">{article.title}</h3>
<p className="text-sm text-gray-600">{article.excerpt}</p>
</a>
))}
</div>
</div>
);
}
// Loading skeletons
function CommentsSkeleton() {
return (
<div className="space-y-4 animate-pulse">
<div className="h-8 bg-gray-200 rounded w-1/3" />
<div className="h-24 bg-gray-200 rounded" />
<div className="h-24 bg-gray-200 rounded" />
</div>
);
}
function RelatedSkeleton() {
return (
<div className="space-y-4">
<div className="h-8 bg-gray-200 rounded w-1/3 animate-pulse" />
<div className="grid grid-cols-3 gap-4">
<div className="h-32 bg-gray-200 rounded animate-pulse" />
<div className="h-32 bg-gray-200 rounded animate-pulse" />
<div className="h-32 bg-gray-200 rounded animate-pulse" />
</div>
</div>
);
}
// Page with multiple Suspense boundaries
export default function ArticlePage({
params
}: {
params: { slug: string }
}) {
return (
<div className="container mx-auto px-4 py-8">
{/* Article shows immediately */}
<ArticleContent slug={params.slug} />
{/* Comments stream in independently */}
<div className="mt-12">
<Suspense fallback={<CommentsSkeleton />}>
<Comments slug={params.slug} />
</Suspense>
</div>
{/* Related articles stream in independently */}
<div className="mt-12">
<Suspense fallback={<RelatedSkeleton />}>
<RelatedArticles slug={params.slug} />
</Suspense>
</div>
</div>
);
}
// ✅ Article content shows first (~500ms)
// ✅ Comments and related articles stream in separately
// ✅ Each section independent - one slow section doesn't block others
// ✅ Progressive enhancementStreaming Benefits
- Instant feedback - users see content immediately
- Better perceived performance - page feels faster
- Independent loading - slow sections don't block fast ones
- SEO-friendly - initial content is in HTML
- Progressive enhancement - works without JavaScript
Automatic Request Deduplication
Next.js automatically deduplicates identical fetch requests in the same render tree.
How It Works
// Multiple components can call the same fetch
async function getUser(id: string) {
console.log('Fetching user:', id); // This only logs once!
const res = await fetch(`https://api.example.com/users/${id}`);
return res.json();
}
// Layout component
async function Layout({ userId }: { userId: string }) {
const user = await getUser(userId); // Request 1
return (
<div>
<Header userName={user.name} />
<Sidebar />
</div>
);
}
// Header component
async function Header({ userName }: { userName: string }) {
// This would normally be another request, but Next.js deduplicates!
const user = await getUser('123'); // Deduplicated - uses cached result
return <header>{user.name}</header>;
}
// Sidebar component
async function Sidebar() {
const user = await getUser('123'); // Also deduplicated!
return <aside>{user.avatar}</aside>;
}
// Console output: "Fetching user: 123" (only once!)
// ✅ Three calls to getUser(), but only ONE actual fetch
// ✅ Automatic optimization by Next.js
// ✅ No manual caching neededReal-World Example: Component Tree
// Shared fetch function
async function getUser(id: string) {
const res = await fetch(`https://api.example.com/users/${id}`);
if (!res.ok) throw new Error('Failed to fetch user');
return res.json();
}
// Page component
async function ProfilePage({ params }: { params: { id: string } }) {
return (
<div className="container mx-auto px-4 py-8">
<ProfileHeader userId={params.id} />
<ProfileStats userId={params.id} />
<ProfilePosts userId={params.id} />
</div>
);
}
// ProfileHeader - needs user data
async function ProfileHeader({ userId }: { userId: string }) {
const user = await getUser(userId); // Fetch 1
return (
<div className="flex items-center gap-4 mb-8">
<img src={user.avatar} alt={user.name} className="w-24 h-24 rounded-full" />
<div>
<h1 className="text-3xl font-bold">{user.name}</h1>
<p className="text-gray-600">{user.bio}</p>
</div>
</div>
);
}
// ProfileStats - also needs user data
async function ProfileStats({ userId }: { userId: string }) {
const user = await getUser(userId); // Deduplicated - no new request!
return (
<div className="grid grid-cols-3 gap-6 mb-8">
<StatCard title="Followers" value={user.followers} />
<StatCard title="Following" value={user.following} />
<StatCard title="Posts" value={user.postsCount} />
</div>
);
}
// ProfilePosts - also needs user data
async function ProfilePosts({ userId }: { userId: string }) {
const user = await getUser(userId); // Also deduplicated!
return (
<div>
<h2 className="text-2xl font-bold mb-4">Posts by {user.name}</h2>
{/* Render posts */}
</div>
);
}
export default ProfilePage;
// ✅ getUser() called 3 times
// ✅ Only ONE actual network request
// ✅ All components get the same data
// ✅ No manual coordination needed🎯 Deduplication Scope
Request deduplication works within a single render pass. If the same request is made in multiple components during one render, only one actual fetch happens.
Preloading Data
Start fetching data early (before it's needed) to reduce wait time.
Pattern 1: Preload in Layout
// Preload function - starts fetch without awaiting
function preloadProduct(id: string) {
// Start the fetch but don't wait for it
void fetch(`https://api.example.com/products/${id}`);
}
// Layout component
export default function ProductLayout({
children,
params,
}: {
children: React.ReactNode;
params: { id: string };
}) {
// Start preloading immediately
preloadProduct(params.id);
return (
<div className="product-layout">
{children}
</div>
);
}
// Child page will use the preloaded/cached data
// app/products/[id]/page.tsx
async function ProductPage({ params }: { params: { id: string } }) {
// This fetch uses the preloaded data - very fast!
const product = await fetch(`https://api.example.com/products/${params.id}`)
.then(r => r.json());
return <ProductDetails product={product} />;
}
// ✅ Fetch starts in layout (early)
// ✅ By the time page renders, data is ready or nearly ready
// ✅ Reduces perceived wait timePattern 2: Parallel Preloading
// Preload multiple resources
function preloadDashboardData(userId: string) {
// Start all fetches simultaneously
void fetch(`https://api.example.com/users/${userId}`);
void fetch(`https://api.example.com/users/${userId}/stats`);
void fetch(`https://api.example.com/users/${userId}/notifications`);
}
export default function DashboardLayout({
children,
params,
}: {
children: React.ReactNode;
params: { userId: string };
}) {
// Preload all dashboard data
preloadDashboardData(params.userId);
return (
<div className="dashboard-layout">
<Sidebar userId={params.userId} />
<main>{children}</main>
</div>
);
}
// Child components will use preloaded data
async function DashboardPage({ params }: { params: { userId: string } }) {
// These fetches are fast because they're preloaded
const [user, stats, notifications] = await Promise.all([
fetch(`https://api.example.com/users/${params.userId}`).then(r => r.json()),
fetch(`https://api.example.com/users/${params.userId}/stats`).then(r => r.json()),
fetch(`https://api.example.com/users/${params.userId}/notifications`).then(r => r.json()),
]);
return (
<div>
<UserHeader user={user} />
<StatsGrid stats={stats} />
<NotificationList notifications={notifications} />
</div>
);
}
// ✅ All data starts loading in layout
// ✅ Page renders with data already available
// ✅ Much faster than waiting until page rendersPattern 3: Waterfall Elimination
// ❌ BAD: Waterfall - each component waits for previous
async function Page() {
return (
<div>
<ComponentA /> {/* Fetches, THEN renders */}
<ComponentB /> {/* Waits for A, THEN fetches */}
<ComponentC /> {/* Waits for B, THEN fetches */}
</div>
);
}
// ✅ GOOD: Preload all data first
async function getPageData() {
return Promise.all([
fetch('https://api.example.com/data-a').then(r => r.json()),
fetch('https://api.example.com/data-b').then(r => r.json()),
fetch('https://api.example.com/data-c').then(r => r.json()),
]);
}
async function Page() {
const [dataA, dataB, dataC] = await getPageData();
return (
<div>
<ComponentA data={dataA} />
<ComponentB data={dataB} />
<ComponentC data={dataC} />
</div>
);
}
// ✅ All data fetched in parallel
// ✅ No waterfall
// ✅ Components render immediately with dataCommon Fetch Patterns
Pattern 1: Fetch Then Render (Default)
// Wait for all data, then render
async function ProductPage({ params }: { params: { id: string } }) {
// Fetch all data first
const [product, reviews, inventory] = await Promise.all([
getProduct(params.id),
getReviews(params.id),
getInventory(params.id),
]);
// Then render with complete data
return (
<div>
<ProductDetails product={product} />
<ReviewsList reviews={reviews} />
<InventoryStatus inventory={inventory} />
</div>
);
}
// ✅ Simple, predictable
// ✅ All data ready at once
// ❌ User waits for everythingPattern 2: Render as You Fetch (Streaming)
import { Suspense } from 'react';
// Render immediately, fetch in background
export default function ProductPage({ params }: { params: { id: string } }) {
return (
<div>
{/* Render product details immediately */}
<ProductDetails productId={params.id} />
{/* Stream in reviews when ready */}
<Suspense fallback={<ReviewsSkeleton />}>
<ReviewsList productId={params.id} />
</Suspense>
{/* Stream in inventory when ready */}
<Suspense fallback={<InventorySkeleton />}>
<InventoryStatus productId={params.id} />
</Suspense>
</div>
);
}
async function ProductDetails({ productId }: { productId: string }) {
const product = await getProduct(productId);
return <div>{/* render product */}</div>;
}
async function ReviewsList({ productId }: { productId: string }) {
const reviews = await getReviews(productId);
return <div>{/* render reviews */}</div>;
}
async function InventoryStatus({ productId }: { productId: string }) {
const inventory = await getInventory(productId);
return <div>{/* render inventory */}</div>;
}
// ✅ User sees something immediately
// ✅ Content streams in progressively
// ✅ Better perceived performancePattern 3: Fetch on Demand (Lazy Loading)
'use client';
import { useState } from 'react';
export function CommentsSection({ postId }: { postId: string }) {
const [comments, setComments] = useState(null);
const [loading, setLoading] = useState(false);
const loadComments = async () => {
setLoading(true);
const data = await fetch(`/api/posts/${postId}/comments`)
.then(r => r.json());
setComments(data);
setLoading(false);
};
return (
<div>
{!comments ? (
<button
onClick={loadComments}
disabled={loading}
className="px-4 py-2 bg-blue-600 text-white rounded"
>
{loading ? 'Loading...' : 'Load Comments'}
</button>
) : (
<div>
{comments.map(comment => (
<Comment key={comment.id} comment={comment} />
))}
</div>
)}
</div>
);
}
// ✅ Doesn't fetch until user requests
// ✅ Reduces initial load
// ❌ Requires Client ComponentPattern 4: Optimistic Updates
'use client';
import { useState, useTransition } from 'react';
export function LikeButton({ postId, initialLikes }: {
postId: string;
initialLikes: number;
}) {
const [likes, setLikes] = useState(initialLikes);
const [isPending, startTransition] = useTransition();
const handleLike = () => {
// Optimistically update UI
setLikes(likes + 1);
// Then update server
startTransition(async () => {
try {
await fetch(`/api/posts/${postId}/like`, {
method: 'POST',
});
} catch (error) {
// Rollback on error
setLikes(likes);
}
});
};
return (
<button
onClick={handleLike}
disabled={isPending}
className="flex items-center gap-2"
>
❤️ {likes} Likes
</button>
);
}
// ✅ Instant UI feedback
// ✅ Server updates in background
// ✅ Rollback on errorData Fetching Strategies
Strategy 1: Critical Path Optimization
async function Page() {
// Priority 1: Critical content (must have)
const criticalData = await getCriticalData();
return (
<div>
{/* Show critical content immediately */}
<CriticalContent data={criticalData} />
{/* Non-critical content streams in */}
<Suspense fallback={<Skeleton />}>
<NonCriticalContent />
</Suspense>
</div>
);
}
// ✅ Users see important content fast
// ✅ Nice-to-have content loads in backgroundStrategy 2: Incremental Loading
async function ProductList({ category }: { category: string }) {
// Load first page immediately
const initialProducts = await getProducts(category, { page: 1, limit: 12 });
return (
<div>
{/* First 12 products */}
<ProductGrid products={initialProducts} />
{/* Load more on demand */}
<LoadMoreButton category={category} />
</div>
);
}
// ✅ Fast initial load
// ✅ Lazy load remaining content
// ✅ Better for large datasetsStrategy 3: Stale While Revalidate
async function NewsFeed() {
// Show cached data immediately
const news = await fetch('https://api.example.com/news', {
next: {
revalidate: 60, // Revalidate every 60 seconds
},
}).then(r => r.json());
return (
<div>
{news.map(article => (
<NewsCard key={article.id} article={article} />
))}
</div>
);
}
// ✅ Instant load from cache
// ✅ Background revalidation
// ✅ Always fresh-ish dataData Fetching Pattern Examples
Examples of streaming, preloading, and other advanced patterns
Select a file or folder to see details
Performance Optimization Checklist
✅ Data Fetching Optimization
- □ Fetch independent requests in parallel
- □ Use Suspense for progressive loading
- □ Preload data in layouts when possible
- □ Leverage automatic request deduplication
- □ Cache static/slow-changing data aggressively
✅ User Experience
- □ Show critical content first
- □ Use meaningful loading skeletons
- □ Stream slow content instead of blocking
- □ Provide immediate feedback for actions
- □ Handle errors gracefully
✅ Code Organization
- □ Extract reusable fetch functions
- □ Co-locate data fetching with components
- □ Use TypeScript for type safety
- □ Document complex fetching patterns
Key Takeaways
- Stream with Suspense - show content progressively
- Automatic deduplication - identical fetches happen once
- Preload data - start fetching early
- Critical path optimization - prioritize important content
- Multiple strategies - choose based on use case
- Fetch then render - simple, all data ready
- Render as you fetch - better perceived performance
- Combine patterns - use multiple strategies together
What's Next?
You've mastered advanced data fetching patterns! Next, we'll dive into Caching and Revalidation—understanding Next.js's caching behavior, cache control options, and strategies for keeping your data fresh while maintaining excellent performance.
Caching is crucial for performance. You'll learn how Next.js caches by default, how to control cache behavior, when to revalidate, and how to build applications that are both fast and always up-to-date.
🎯 Pattern Selection
No single pattern is always best. Streaming with Suspense is great for user-facing pages with mixed content speeds. Fetch then render works well for dashboards where all data should be ready. Choose patterns based on your specific use case and user needs!