Server Components help only when the server-client boundary is intentional. Here is the practical model I use to keep data safe and browser JavaScript small.

React Server Components are not a speed button.

Move every component to the server and a real application will still have slow queries, oversized images, duplicated requests, and awkward interactions.

The useful change is smaller: Server Components force us to decide where each responsibility belongs.

That boundary is what makes the architecture better.

Server rendering and Server Components are not the same

I see these terms mixed together constantly.

Server-side rendering creates HTML for an initial response. A Client Component can still be server-rendered and then hydrated in the browser.

A React Server Component is different. Its component code does not become part of the client JavaScript bundle. It can read server-side data and compose the initial interface, but it cannot use browser state, effects, or event handlers.

That gives me a simple first question:

Does this component need a browser capability, or does it only need to read data and render?

If it only reads and renders, I start on the server.

The boundary I use on client projects

The server side owns:

  • authentication and authorization checks;
  • database and private API access;
  • secret-bearing provider SDKs;
  • data normalization;
  • the first meaningful content;
  • decisions that must not be trusted to the browser.

The client side owns:

  • click, input, drag, and keyboard interactions;
  • local optimistic state;
  • browser-only APIs;
  • animation that depends on live user input;
  • small islands that genuinely need hydration.

This is not a purity contest. A form can be a Client Component while the page around it stays on the server.

Pass a safe DTO, not a database record

The most important implementation detail is the data crossing the boundary.

type PublicAccount = {
  displayName: string;
  planLabel: string;
};

async function getPublicAccount(userId: string): Promise<PublicAccount> {
  const account = await db.account.findUniqueOrThrow({
    where: { userId },
    select: {
      displayName: true,
      plan: true,
    },
  });

  return {
    displayName: account.displayName,
    planLabel: account.plan,
  };
}

export default async function AccountPage() {
  const session = await requireSession();
  const account = await getPublicAccount(session.user.id);

  return <AccountSwitcher initialAccount={account} />;
}

The browser receives the two fields it needs.

It does not receive internal IDs, billing records, provider tokens, or a full object that happened to be convenient.

My rule: if a value crosses into a Client Component, treat it as public to that user. TypeScript does not make an unsafe payload private.

Composition keeps the client boundary small

A common mistake is adding "use client" to a layout because one nested button needs state. That turns the whole imported subtree into client code.

I keep the interactive leaf small instead:

export default async function ProjectPage() {
  const project = await getProject();

  return (
    <article>
      <ProjectSummary project={project} />
      <ProjectActions projectId={project.id} />
    </article>
  );
}

ProjectSummary can stay on the server. ProjectActions can be the focused Client Component.

The page remains readable without waiting for a client-side data request, while the action area still behaves like an application.

What Server Components do not solve

They do not automatically fix:

  • sequential database calls;
  • missing cache or revalidation decisions;
  • slow origin response time;
  • unbounded work on the server;
  • poor image sizing;
  • layout shifts;
  • insecure mutations.

They can even hide bad decisions. Moving a slow request to the server may remove a loading spinner from the component, but the user still waits for the response.

Measure the actual path.

The practical verdict

I use Server Components as an ownership tool:

  1. fetch and authorize close to the server truth;
  2. shape the smallest safe DTO;
  3. render useful content before client JavaScript;
  4. hydrate only the interaction that needs it;
  5. measure the complete request, not the component in isolation.

That model is less exciting than calling every component “server-first.”

It is also much easier to maintain.

For a deeper implementation checklist, read how I draw Next.js server and client boundaries. The current framework details are also documented in the official Next.js Server and Client Components guide.

If an existing React application has grown into one giant client boundary, send me the architecture problem. I will tell you what I would separate first.

  • #React
  • #Server Components
  • #Next.js
  • #Performance
  • #Security
M H Tawfik (Al Mojakkar Hossain Tawfik)

M H Tawfik (Al Mojakkar Hossain Tawfik)

Al Mojakkar Hossain Tawfik, professionally known as M H Tawfik and Tawfik, is a freelance Full-Stack Web Developer and the founder of SoftWebGrove. His legal-document name is Al Mojakkar Hossain.

Continue reading

Related field notes on engineering, SaaS, freelancing, and building dependable products.