mdashikjs/blog
All posts
Next.js Server Components: Data Fetching Without the Waterfall
Frontend

Next.js Server Components: Data Fetching Without the Waterfall

Frontend4 min

Next.js Server Components: Data Fetching Without the Waterfall

Server Components changed how I think about data fetching in Next.js. The key insight: fetch where you render, not in a top-level loader. But there are traps if you are not careful about parallelism.

Next.jsReactServer ComponentsPerformance
Share:

The old way: data down, components up

In the Pages Router, getServerSideProps fetched everything at the top and passed it down through props. This meant the page component knew about every piece of data the entire tree needed.

The new way: colocate data and UI

With Server Components, each component fetches its own data:

async function UserProfile({ userId }: { userId: string }) {
  const user = await getUser(userId);
  return <div>{user.name}</div>;
}

async function UserPosts({ userId }: { userId: string }) {
  const posts = await getUserPosts(userId);
  return (
    <ul>
      {posts.map(post => <li key={post.id}>{post.title}</li>)}
    </ul>
  );
}

Each component is self-contained. Remove UserPosts from the page and its data fetch disappears too.

The waterfall trap

If you nest async components, they serialize:

// This waterfalls — UserPosts waits for UserProfile to finish
async function Page({ userId }: { userId: string }) {
  return (
    <div>
      <UserProfile userId={userId} />
      <UserPosts userId={userId} />
    </div>
  );
}

Fix: fetch in parallel at the page level and pass the promises:

async function Page({ userId }: { userId: string }) {
  const userPromise = getUser(userId);
  const postsPromise = getUserPosts(userId);

  return (
    <div>
      <UserProfile userPromise={userPromise} />
      <UserPosts postsPromise={postsPromise} />
    </div>
  );
}

Both fetches start immediately. Each component awaits its own promise.

When to use Suspense boundaries

Wrap slow components in Suspense so the fast parts render immediately:

<Suspense fallback={<PostsSkeleton />}>
  <UserPosts postsPromise={postsPromise} />
</Suspense>

The profile renders instantly. Posts stream in when ready.

What stays client-side

Anything with useState, useEffect, event handlers, or browser APIs. The rule is simple: if it reacts to user interaction, it is a Client Component. If it just renders data, keep it on the server.

MA

Written by Md Ashik

Senior Software Engineer building reliable backends. I write about the practical tradeoffs behind shipping software that holds up in production.