Understanding the Waterfall Effect in Next.js (and How to Fix It)


Why Do Next.js Websites Load Slowly?
Building fast, responsive websites is every developer’s goal. But sometimes, a website that should load in 5 seconds ends up taking 15 seconds or more.
One common reason for this in Next.js is the waterfall effect. This happens when some API calls are made sequentially instead of in parallel, slowing everything down.
In this article, I’ll break down the waterfall effect with a simple analogy, explain why it affects performance, and share practical ways to fix it.
What is the Waterfall Effect?
The waterfall effect occurs when each API call waits for the previous one to finish before starting. Instead of loading resources in parallel, requests stack up one after another like water falling step by step.
This makes your site slower and increases time to first render, leading to poor user experience and higher bounce rates.
A Restaurant Analogy 🍽️
Let’s make this simple. Imagine you and three friends (3 API calls) go to a restaurant.
Normally, the waiter should take all orders at once and deliver the meals together. That saves time.
But in the waterfall effect scenario:
The waiter takes the first order, prepares it for 5 minutes, but doesn’t deliver it.
Then takes the second order, prepares it for 5 minutes, and still doesn’t deliver it.
Finally takes the third order, prepares it for 5 minutes, and then delivers all meals together.
Now, instead of waiting 5 minutes total, you end up waiting 15 minutes.
That’s how the waterfall effect delays your website.
If one API call takes 500ms,
Three sequential calls take 500ms × 3 = 1500ms.
👉 Bad performance.
➡️ Example:
//❌ Sequential (Waterfall Effect)
const users = await fetch("/api/users").then(res => res.json());
const posts = await fetch("/api/posts").then(res => res.json());
const comments = await fetch("/api/comments").then(res => res.json());
Here:
Request 2 waits for Request 1
Request 3 waits for Request 2
Total time = sum of all requests (slow 🚶🏾♂️).
When Waterfalls Are Useful
Sometimes, sequential requests are necessary.
📌 Example:
Fetch a user’s profile → get their ID
Use the ID to fetch their list of friends
async function fetchUserData() {
// Step 1: Fetch user profile
const userProfile = await fetch('/api/user-profile').then(res => res.json());
// Step 2: Use the user ID to fetch their friends
const friends = await fetch(`/api/friends/${userProfile.id}`).then(res => res.json());
// Step 3: Use the friends list to fetch posts
const posts = await fetch(`/api/posts?friends=${friends.map(f => f.id).join(',')}`)
.then(res => res.json());
console.log({ userProfile, friends, posts });
}
fetchUserData();
In the restaurant analogy, it’s like asking each person what they want to eat, but the three friends need to confirm sequentially whether the previous dish is good before placing their own order. The waiter can’t start preparing the next meal until the status of the previous one is confirmed.
How to Fix the Waterfall Effect
The good news? You can fix this with parallel fetching and streaming.
1. Using Promise.all()
With Promise.all()
, all API calls run at the same time. The waiter takes all orders at once, prepares everything in parallel, and then delivers the meals together.
async function fetchData() {
const [data1, data2, data3] = await Promise.all([
fetch('/api/first').then(res => res.json()),
fetch('/api/second').then(res => res.json()),
fetch('/api/third').then(res => res.json())
]);
console.log(data1, data2, data3);
}
✅ Faster than sequential calls
⚠️ Downside: If one API call is slow, all others wait until it finishes
Example: Instead of 500ms total, it may take 700ms if one request lags behind.
2. Using React Suspense
React Suspense solves the waiting problem. It still makes all requests at once, but instead of waiting for all responses, it streams results progressively. By streaming, you can prevent slow data requests from blocking your whole page. This allows the user to see and interact with parts of the page without waiting for all the data to load before any UI can be shown to the user. So, as regards the analogy, as soon as one meal is ready, it’s delivered without waiting for the others.
Streaming works well with React's component model, as each component can be considered a chunk.
import { Suspense } from "react";
function Page() {
return (
<div>
<Suspense fallback={<p>Loading user...</p>}>
<User />
</Suspense>
<Suspense fallback={<p>Loading posts...</p>}>
<Posts />
</Suspense>
</div>
);
}
✅ Users see content faster
✅ Improves perceived performance
🚀 Best for user experience
⚠️ Note: Suspense doesn’t work with client-side API SDKs (like Firebase). In such cases, you need to manage a loading state manually and stream each API’s data into the UI as it becomes available.
3. Using Loading States for Client APIs (e.g., Firebase)
When working with client-based APIs or SDKs (like Firebase), you can’t rely on Suspense. Instead, you manage loading states manually.
This is like the waiter preparing all orders but delivering meals one by one as soon as each is ready, so no one at the table stays hungry waiting for the slowest dish(same as React Suspense but for client-based APIs).
import { useEffect, useState } from "react";
import { getDocs, collection } from "firebase/firestore";
import { db } from "./firebase";
function Posts() {
const [posts, setPosts] = useState([]);
const [loading, setLoading] = useState(true);
useEffect(() => {
async function fetchPosts() {
const snapshot = await getDocs(collection(db, "posts"));
const data = snapshot.docs.map(doc => doc.data());
setPosts(data);
setLoading(false);
}
fetchPosts();
}, []);
if (loading) return <p>Loading posts...</p>;
return (
<ul>
{posts.map((post, i) => (
<li key={i}>{post.title}</li>
))}
</ul>
);
}
✅ Works with client SDKs like Firebase
✅ Streams each API response into the UI using a loading state
⚠️ More manual work compared to Suspense
Key Takeaways
Waterfall effect = sequential API calls = 🚶 slow websites.
Promise.all() = parallel API calls = 🚴 faster websites.
Suspense = progressive streaming = 🚀 best UX (but not for client SDKs).
Loading states for client SDKs = stream results to UI manually when using APIs like Firebase.
Conclusion
Website performance is crucial for Next.js apps. A slow site not only frustrates users but also affects SEO ranking, engagement, and conversion rates.
By avoiding the waterfall effect, you ensure faster loading times and smoother experiences. Use Promise.all()
for parallel fetching, and take advantage of React Suspense for progressive loading.
If you want your Next.js apps to feel snappy and responsive, remember:
👉 Don’t let your APIs fall like a waterfall.
✨ Thanks for reading! If you found this helpful, share it with another developer and let’s build faster web apps together.
Subscribe to my newsletter
Read articles from Ogunleye Odunayo directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by
