DEV Community

Anas Sheikh
Anas Sheikh

Posted on

Your Server Component Might Be Fetching Data Three Times Slower Than It Needs To

This is one of the easiest performance mistakes to write without noticing, since the code reads completely naturally, top to bottom, each line waiting for the one before it to finish, exactly the way most people learn to write async code in the first place.

The Setup That Reads Perfectly Naturally

// app/dashboard/page.tsx
export default async function DashboardPage() {
  const user = await getUser();
  const posts = await getPosts();
  const comments = await getComments();
  const analytics = await getAnalytics();

  return (
    <Dashboard
      user={user}
      posts={posts}
      comments={comments}
      analytics={analytics}
    />
  );
}
Enter fullscreen mode Exit fullscreen mode

Four await calls, one after another. Clean, readable, and exactly how sequential code naturally gets written when each line is just "get the next thing I need." The problem is that none of these four calls actually depend on the results of the others, getPosts doesn't need anything from getUser, getComments doesn't need anything from getPosts, and so on. They're all independently fetchable at the same time, and writing them sequentially means the total page load time is the sum of all four, not the time of the single slowest one.

Why This Costs More Than It Looks Like It Should

If each of these four calls takes roughly 150 milliseconds, a genuinely reasonable time for a real database query, the sequential version takes roughly 600 milliseconds total, each one waiting for the previous one to fully complete before even starting. Run the same four calls concurrently instead, and the total time is roughly 150 milliseconds, the time of the single slowest call, since they're all genuinely happening at once rather than queued up one behind another for no actual reason.

This is a real, measurable difference on a page real users are waiting for, and it's entirely avoidable, since nothing about these four pieces of data actually requires this sequential order, the code just happens to be written in a shape that imposes one anyway.

The Actual Fix: Promise.all for Independent Fetches

export default async function DashboardPage() {
  const [user, posts, comments, analytics] = await Promise.all([
    getUser(),
    getPosts(),
    getComments(),
    getAnalytics(),
  ]);

  return (
    <Dashboard
      user={user}
      posts={posts}
      comments={comments}
      analytics={analytics}
    />
  );
}
Enter fullscreen mode Exit fullscreen mode

Promise.all starts all four requests essentially simultaneously and waits for all of them to complete together, rather than starting each one only after the last one finished. Same data, same final render, a fraction of the total wait, purely by removing an ordering constraint that was never actually necessary in the first place.

Where This Gets Genuinely Trickier: Partial Dependencies

Not every case is this clean. Sometimes one piece of data genuinely depends on another, and mixing dependent and independent fetches needs a bit more care than a single flat Promise.all.

// ❌ Everything sequential, even though posts and analytics don't need each other
export default async function DashboardPage() {
  const user = await getUser();
  const posts = await getPostsForUser(user.id); // genuinely depends on user
  const analytics = await getAnalytics(); // does NOT depend on user at all
  return <Dashboard user={user} posts={posts} analytics={analytics} />;
}
Enter fullscreen mode Exit fullscreen mode
// ✅ Only the genuine dependency stays sequential, everything else runs alongside it
export default async function DashboardPage() {
  const analyticsPromise = getAnalytics(); // starts immediately, not awaited yet

  const user = await getUser();
  const posts = await getPostsForUser(user.id); // genuinely needs user.id first

  const analytics = await analyticsPromise; // was already running this whole time

  return <Dashboard user={user} posts={posts} analytics={analytics} />;
}
Enter fullscreen mode Exit fullscreen mode

Starting getAnalytics() without immediately awaiting it lets that request begin running in the background while getUser and getPostsForUser proceed through their genuine, necessary sequence. By the time the code actually needs the analytics result, it's very likely already finished, or at least has been running the whole time instead of only starting after everything else completed.

How to Actually Spot This in Your Own Code

Look for multiple await statements in a row where each one calls a genuinely independent function, nothing on the left side of an earlier line appears as an argument to a later one. That absence of a real data dependency is the signal, these calls have no actual reason to be sequential, and wrapping them in Promise.all is very likely a free, meaningful performance improvement with zero behavior change.

grep -B2 -A2 "await get" app/**/page.tsx
Enter fullscreen mode Exit fullscreen mode

Scanning for clusters of consecutive await calls to different functions, then checking whether any of them genuinely reference the previous line's result, is a quick, practical way to find real waterfall candidates across an existing codebase.

Why This Matters More on Pages With Several Independent Data Sources

A dashboard, a page with several distinct widgets or sections each backed by their own query, is exactly where this pattern accumulates the most cost, since it's common to have four, five, or more genuinely independent pieces of data feeding one page, each one innocently written as its own await line, each one adding its full latency on top of all the others instead of overlapping.

The Actual Rule

Before awaiting multiple async calls sequentially, check whether any of them genuinely need a previous call's result. If none do, Promise.all turns their combined wait time from a sum into a maximum, for free, with no behavior change. For a genuine mix of dependent and independent calls, start the independent ones early without immediately awaiting them, so they run in the background while the real, necessary sequence proceeds.

I check every Server Component I write for exactly this pattern now, across client work and the dashboards and templates I build at pixelanas.com, since it's one of the highest-value, lowest-effort performance fixes available, and I've written about related data-fetching patterns on the blog too.


Go check your own dashboard or any data-heavy page for consecutive await calls with no genuine dependency between them. If you find some, wrapping them in Promise.all is very likely a free win sitting right there. Drop what you find in the comments.

Get the templates: https://pixelanas.gumroad.com


Anas, full-stack Next.js developer building SaaS products and premium templates. X: @ASheikh69751

Top comments (0)