The way you organize your data fetching can make the difference between a fast page and a slow one. Parallel fetching lets independent requests run simultaneously, while sequential fetching waits for each request to complete before starting the next. Understanding when to use each approach is critical for performance. Fetch data in parallel when possible, sequential when necessary. Let's master both patterns and learn to optimize your data fetching strategy!
The Problem: Request Waterfalls
❌ Sequential Fetching (Slow)
async function DashboardPage() {
// Request 1: Wait 1000ms
const user = await fetch('https://api.example.com/user')
.then(r => r.json());
// Request 2: Wait another 800ms (starts after Request 1)
const posts = await fetch('https://api.example.com/posts')
.then(r => r.json());
// Request 3: Wait another 600ms (starts after Request 2)
const notifications = await fetch('https://api.example.com/notifications')
.then(r => r.json());
return <Dashboard user={user} posts={posts} notifications={notifications} />;
}
// ⏱️ Total time: 1000ms + 800ms + 600ms = 2400ms
// ❌ Requests happen one after another (waterfall)
// ❌ Each request waits for the previous oneRequest Timeline (Sequential):
✅ Parallel Fetching (Fast)
async function DashboardPage() {
// All requests start simultaneously
const [user, posts, notifications] = await Promise.all([
fetch('https://api.example.com/user').then(r => r.json()),
fetch('https://api.example.com/posts').then(r => r.json()),
fetch('https://api.example.com/notifications').then(r => r.json()),
]);
return <Dashboard user={user} posts={posts} notifications={notifications} />;
}
// ⏱️ Total time: 1000ms (slowest request)
// ✅ All requests happen simultaneously
// ✅ Total time = time of slowest requestRequest Timeline (Parallel):
The Performance Impact
In this example, parallel fetching is 60% faster (1000ms vs 2400ms). For pages with many requests, the difference can be even more dramatic!
Parallel Data Fetching
Use parallel fetching when requests are independent and don't depend on each other.
Pattern 1: Promise.all() with Array
async function DashboardPage() {
// Fetch multiple independent data sources in parallel
const [user, stats, recentOrders, notifications] = await Promise.all([
fetch('https://api.example.com/user').then(r => r.json()),
fetch('https://api.example.com/stats').then(r => r.json()),
fetch('https://api.example.com/orders/recent').then(r => r.json()),
fetch('https://api.example.com/notifications').then(r => r.json()),
]);
return (
<div className="dashboard">
<UserProfile user={user} />
<StatsGrid stats={stats} />
<RecentOrders orders={recentOrders} />
<NotificationList notifications={notifications} />
</div>
);
}
// ✅ All 4 requests start simultaneously
// ✅ Total time = slowest request
// ✅ Much faster than sequentialPattern 2: Promise.all() with Named Variables
async function ProductsPage() {
// Start all requests simultaneously
const productsPromise = fetch('https://api.example.com/products')
.then(r => r.json());
const categoriesPromise = fetch('https://api.example.com/categories')
.then(r => r.json());
const featuredPromise = fetch('https://api.example.com/products/featured')
.then(r => r.json());
// Wait for all to complete
const [products, categories, featured] = await Promise.all([
productsPromise,
categoriesPromise,
featuredPromise,
]);
return (
<div>
<FeaturedProducts products={featured} />
<CategoryFilter categories={categories} />
<ProductGrid products={products} />
</div>
);
}
// ✅ More explicit - shows intent clearly
// ✅ Easy to add more requests
// ✅ Same performance as previous patternPattern 3: Parallel Fetch with Reusable Functions
// Reusable fetch functions
export 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();
}
export async function getUserPosts(userId: string) {
const res = await fetch(`https://api.example.com/users/${userId}/posts`);
if (!res.ok) throw new Error('Failed to fetch posts');
return res.json();
}
export async function getUserFollowers(userId: string) {
const res = await fetch(`https://api.example.com/users/${userId}/followers`);
if (!res.ok) throw new Error('Failed to fetch followers');
return res.json();
}import { getUser, getUserPosts, getUserFollowers } from '@/lib/api';
async function ProfilePage({ params }: { params: { id: string } }) {
// Fetch all data in parallel using reusable functions
const [user, posts, followers] = await Promise.all([
getUser(params.id),
getUserPosts(params.id),
getUserFollowers(params.id),
]);
return (
<div className="profile">
<ProfileHeader user={user} followerCount={followers.length} />
<PostList posts={posts} />
<FollowerList followers={followers} />
</div>
);
}
export default ProfilePage;
// ✅ Clean, reusable functions
// ✅ Type-safe
// ✅ Parallel execution
// ✅ Easy to testReal-World Example: E-commerce Product Page
interface Product {
id: string;
title: string;
price: number;
description: string;
category: string;
}
interface Review {
id: string;
rating: number;
comment: string;
author: string;
}
interface RelatedProduct {
id: string;
title: string;
price: number;
image: string;
}
async function ProductPage({ params }: { params: { id: string } }) {
// Fetch 4 independent data sources in parallel
const [product, reviews, relatedProducts, inventory] = await Promise.all([
// Product details
fetch(`https://api.example.com/products/${params.id}`)
.then(r => r.json()) as Promise<Product>,
// Product reviews
fetch(`https://api.example.com/products/${params.id}/reviews`)
.then(r => r.json()) as Promise<Review[]>,
// Related products
fetch(`https://api.example.com/products/${params.id}/related`)
.then(r => r.json()) as Promise<RelatedProduct[]>,
// Inventory status
fetch(`https://api.example.com/products/${params.id}/inventory`)
.then(r => r.json()),
]);
// Calculate average rating
const avgRating = reviews.reduce((sum, r) => sum + r.rating, 0) / reviews.length;
return (
<div className="container mx-auto px-4 py-8">
<div className="grid grid-cols-1 lg:grid-cols-2 gap-8">
{/* Product Info */}
<div>
<h1 className="text-4xl font-bold mb-4">{product.title}</h1>
<div className="flex items-center gap-2 mb-4">
<span className="text-2xl">⭐</span>
<span className="text-lg font-semibold">
{avgRating.toFixed(1)} ({reviews.length} reviews)
</span>
</div>
<p className="text-3xl text-green-600 mb-6">${product.price}</p>
<p className="text-gray-700 mb-6">{product.description}</p>
{/* Inventory Status */}
<div className="mb-6">
{inventory.inStock ? (
<span className="text-green-600 font-semibold">
✓ In Stock ({inventory.quantity} available)
</span>
) : (
<span className="text-red-600 font-semibold">Out of Stock</span>
)}
</div>
<button className="w-full bg-blue-600 text-white py-3 rounded-lg font-semibold">
Add to Cart
</button>
</div>
{/* Product Image */}
<div>
<img
src={product.image}
alt={product.title}
className="w-full rounded-lg"
/>
</div>
</div>
{/* Reviews Section */}
<div className="mt-12">
<h2 className="text-2xl font-bold mb-6">Customer Reviews</h2>
<div className="space-y-4">
{reviews.map(review => (
<div key={review.id} className="border rounded-lg p-4">
<div className="flex items-center gap-2 mb-2">
<span className="text-yellow-500">{'⭐'.repeat(review.rating)}</span>
<span className="font-semibold">{review.author}</span>
</div>
<p className="text-gray-700">{review.comment}</p>
</div>
))}
</div>
</div>
{/* Related Products */}
<div className="mt-12">
<h2 className="text-2xl font-bold mb-6">You May Also Like</h2>
<div className="grid grid-cols-2 md:grid-cols-4 gap-6">
{relatedProducts.map(related => (
<a
key={related.id}
href={`/products/${related.id}`}
className="border rounded-lg p-4 hover:shadow-lg transition"
>
<img
src={related.image}
alt={related.title}
className="w-full h-48 object-cover rounded mb-3"
/>
<h3 className="font-semibold mb-2">{related.title}</h3>
<p className="text-green-600">${related.price}</p>
</a>
))}
</div>
</div>
</div>
);
}
export default ProductPage;
// ✅ 4 requests in parallel
// ✅ Page loads 70-80% faster than sequential
// ✅ All data available immediately
// ✅ Better user experienceSequential Data Fetching
Use sequential fetching when one request depends on data from a previous request.
When Sequential Fetching is Necessary
async function UserPage({ params }: { params: { id: string } }) {
// Step 1: Get user data first
const user = await fetch(`https://api.example.com/users/${params.id}`)
.then(r => r.json());
// Step 2: Get user's posts (depends on user data)
// We need user.postsUrl or specific user info
const posts = await fetch(
`https://api.example.com/users/${user.id}/posts?lang=${user.preferredLanguage}`
).then(r => r.json());
// Step 3: Get post statistics (depends on post IDs)
const postIds = posts.map(p => p.id).join(',');
const analytics = await fetch(
`https://api.example.com/analytics?postIds=${postIds}`
).then(r => r.json());
return (
<div>
<UserProfile user={user} />
<PostList posts={posts} analytics={analytics} />
</div>
);
}
// ⏱️ Slower, but necessary
// ✅ Each request needs data from the previous one
// ✅ Cannot be parallelizedExample: Nested Dependencies
interface Team {
id: string;
name: string;
memberIds: string[];
}
interface Member {
id: string;
name: string;
email: string;
}
interface Project {
id: string;
title: string;
status: string;
}
async function TeamPage({ params }: { params: { teamId: string } }) {
// Level 1: Get team info
const team: Team = await fetch(
`https://api.example.com/teams/${params.teamId}`
).then(r => r.json());
// Level 2: Get team members (needs team.memberIds)
const members: Member[] = await fetch(
`https://api.example.com/users?ids=${team.memberIds.join(',')}`
).then(r => r.json());
// Level 3: Get each member's projects (needs member IDs)
// This CAN be parallelized since all member requests are independent
const projectsPromises = members.map(member =>
fetch(`https://api.example.com/users/${member.id}/projects`)
.then(r => r.json())
);
const allProjects: Project[][] = await Promise.all(projectsPromises);
return (
<div className="container mx-auto px-4 py-8">
<h1 className="text-3xl font-bold mb-8">{team.name}</h1>
<div className="space-y-8">
{members.map((member, index) => (
<div key={member.id} className="border rounded-lg p-6">
<h2 className="text-xl font-semibold mb-2">{member.name}</h2>
<p className="text-gray-600 mb-4">{member.email}</p>
<h3 className="font-semibold mb-2">Projects:</h3>
<div className="space-y-2">
{allProjects[index].map(project => (
<div key={project.id} className="flex items-center gap-2">
<span className="font-medium">{project.title}</span>
<span className={
project.status === 'active'
? 'text-green-600'
: 'text-gray-500'
}>
• {project.status}
</span>
</div>
))}
</div>
</div>
))}
</div>
</div>
);
}
export default TeamPage;
// Step 1: Team (sequential - must be first)
// Step 2: Members (sequential - needs team.memberIds)
// Step 3: Projects (parallel - all independent)
// ✅ Optimized: Sequential where needed, parallel where possibleMixed Approach: Optimizing Both
The best approach often combines parallel and sequential fetching strategically.
Pattern: Parallel Groups in Sequence
async function AdvancedDashboard() {
// Group 1: Initial data (parallel)
const [user, config] = await Promise.all([
fetch('https://api.example.com/user').then(r => r.json()),
fetch('https://api.example.com/config').then(r => r.json()),
]);
// Group 2: User-specific data (parallel, but depends on user)
const [posts, followers, notifications] = await Promise.all([
fetch(`https://api.example.com/users/${user.id}/posts`).then(r => r.json()),
fetch(`https://api.example.com/users/${user.id}/followers`).then(r => r.json()),
fetch(`https://api.example.com/users/${user.id}/notifications`).then(r => r.json()),
]);
// Group 3: Analytics (parallel, depends on posts)
const postIds = posts.map(p => p.id);
const [analytics, engagement] = await Promise.all([
fetch(`https://api.example.com/analytics?postIds=${postIds.join(',')}`)
.then(r => r.json()),
fetch(`https://api.example.com/engagement?postIds=${postIds.join(',')}`)
.then(r => r.json()),
]);
return (
<div className="dashboard">
<UserHeader user={user} />
<PostGrid posts={posts} analytics={analytics} />
<EngagementStats engagement={engagement} />
<FollowerList followers={followers} />
<NotificationCenter notifications={notifications} />
</div>
);
}
// Timeline:
// Group 1: user + config (parallel) - 500ms
// Group 2: posts + followers + notifications (parallel) - 800ms
// Group 3: analytics + engagement (parallel) - 600ms
// Total: 500 + 800 + 600 = 1900ms
//
// If all sequential: 500 + 400 + 600 + 300 + 800 + 600 = 3200ms
// Savings: 1300ms (40% faster!)Pattern: Optimize Critical Path
async function ArticlePage({ params }: { params: { slug: string } }) {
// Critical: Get article first (user needs to see content)
const article = await fetch(
`https://api.example.com/articles/${params.slug}`
).then(r => r.json());
// Non-critical: Get supplementary data in parallel
// These enhance the experience but aren't essential
const [author, relatedArticles, comments] = await Promise.all([
fetch(`https://api.example.com/users/${article.authorId}`)
.then(r => r.json()),
fetch(`https://api.example.com/articles/${params.slug}/related`)
.then(r => r.json()),
fetch(`https://api.example.com/articles/${params.slug}/comments`)
.then(r => r.json()),
]);
return (
<article className="container mx-auto px-4 py-8">
{/* Critical content - shows first */}
<h1 className="text-4xl font-bold mb-4">{article.title}</h1>
<div
className="prose prose-lg mb-8"
dangerouslySetInnerHTML={{ __html: article.content }}
/>
{/* Supplementary content */}
<AuthorBio author={author} />
<RelatedArticles articles={relatedArticles} />
<CommentSection comments={comments} />
</article>
);
}
// ✅ Article content available quickly
// ✅ Supplementary data loads in parallel
// ✅ Optimized for user experiencePerformance Comparison
| Scenario | Requests | Sequential Time | Parallel Time | Improvement |
|---|---|---|---|---|
| Dashboard (3 requests) | 3 | 2400ms | 1000ms | 58% faster |
| Product page (4 requests) | 4 | 3200ms | 900ms | 72% faster |
| User profile (5 requests) | 5 | 4000ms | 1200ms | 70% faster |
Performance Impact
Parallel fetching typically provides 50-75% performance improvement for pages with multiple independent requests. This translates to noticeably faster page loads and better user experience.
Fetching Pattern Examples
Examples of parallel, sequential, and mixed fetching patterns
Select a file or folder to see details
Decision Guide: Parallel vs Sequential
Use Parallel Fetching When:
- ✅ Requests are independent
- ✅ No request needs data from another
- ✅ All data is needed at the same time
- ✅ You want the fastest possible load time
Examples:
- Dashboard with user, stats, notifications
- Product page with product, reviews, related items
- Profile page with user, posts, followers
Use Sequential Fetching When:
- ⚠️ One request depends on another's data
- ⚠️ You need to process data between requests
- ⚠️ Requests must happen in a specific order
- ⚠️ Later requests need IDs or URLs from earlier ones
Examples:
- User → User's posts → Post analytics
- Team → Team members → Member projects
- Article → Author info → Author's other articles
Use Mixed Approach When:
- 🎯 Some requests depend on others, some don't
- 🎯 You can group independent requests together
- 🎯 You want to optimize a complex dependency chain
Pattern:
- Fetch critical data first (sequential if needed)
- Group independent requests and fetch in parallel
- Fetch dependent data in next sequential step
- Repeat as needed
Best Practices
1. Default to Parallel
// ✅ GOOD: Parallel by default
const [a, b, c] = await Promise.all([getA(), getB(), getC()]);
// ❌ BAD: Sequential without reason
const a = await getA();
const b = await getB();
const c = await getC();2. Identify Dependencies
// ✅ GOOD: Clear dependencies
const user = await getUser(id); // Must be first
const [posts, followers] = await Promise.all([
getUserPosts(user.id), // Depends on user
getUserFollowers(user.id), // Depends on user
]);
// Both posts and followers depend on user but not each other
// So fetch them in parallel after getting user3. Optimize Critical Path
// ✅ GOOD: Critical content first
const article = await getArticle(slug); // Critical
const [comments, related] = await Promise.all([
getComments(slug), // Nice to have
getRelated(slug), // Nice to have
]);
// User sees article quickly, supplementary data loads in parallel4. Handle Errors Independently
// ✅ GOOD: Each request handles its own errors
const [user, posts, notifications] = await Promise.all([
getUser(id),
getUserPosts(id).catch(() => []), // Return empty array on error
getNotifications(id).catch(() => []), // Don't fail entire page
]);
// If posts fail, page still works with user and notifications5. Use Promise.allSettled for Optional Data
// When some requests might fail but page should still work
const results = await Promise.allSettled([
fetch('https://api.example.com/critical'),
fetch('https://api.example.com/optional1'),
fetch('https://api.example.com/optional2'),
]);
// Check each result
const critical = results[0].status === 'fulfilled'
? await results[0].value.json()
: null;
const optional1 = results[1].status === 'fulfilled'
? await results[1].value.json()
: null;
// Page works even if optional requests failKey Takeaways
- Parallel fetching is faster - total time = slowest request
- Sequential creates waterfalls - total time = sum of all requests
- Use Promise.all() - fetch independent requests in parallel
- Sequential when dependencies exist - one request needs another's data
- Mix both approaches - optimize complex dependency chains
- 50-75% performance improvement - parallel is dramatically faster
- Default to parallel - only go sequential when necessary
- Handle errors gracefully - don't let one failure break everything
What's Next?
You've mastered parallel and sequential data fetching! Next, we'll explore Data Fetching Patterns and Strategies—advanced patterns including streaming with Suspense, preloading, and more sophisticated optimization techniques.
These advanced patterns will help you build even faster applications by strategically loading data, streaming content to users progressively, and optimizing the user experience further.
⚡ Performance Rule
When in doubt, fetch in parallel. Sequential fetching should be the exception, not the rule. Always ask: "Does this request truly depend on the previous one?" If not, fetch in parallel!