Writing About Contact
Deep Dive 14 min read

React Server Components: One Year In Production

RSC promised to solve data fetching and bundle size simultaneously. After a year of production usage, here's what held up, what didn't, and the mental model shift required to use them well.

Honey Sharma

React Server Components have been stable for a year now, and we’ve shipped three significant products using the App Router’s RSC model. The promise was compelling: co-locate your data fetching with your UI components, ship zero JS for server-rendered content, eliminate the waterfall of client-side data fetching.

The promise delivered — with significant caveats.

The Mental Model Shift

The hardest part of RSC isn’t the API. It’s resetting your intuitions about where code runs.

In traditional React, the entire component tree runs in the browser. State lives in the browser. Effects run in the browser. This is your mental model after years of React development.

With RSC, components have a new axis: server vs. client. Server components run once on the server during rendering. They can be async, access databases directly, read environment variables. They never run in the browser.

// This runs ONLY on the server — no JS shipped to client
async function BlogPost({ slug }: { slug: string }) {
  const post = await db.posts.findOne({ slug }); // Direct DB access
  
  return (
    <article>
      <h1>{post.title}</h1>
      <p>{post.content}</p>
      {/* This component ships JS */}
      <LikeButton postId={post.id} client />
    </article>
  );
}

What Actually Improved

Data fetching: Eliminating the client-side data fetching waterfall is real and significant. Our TTFB improved, but more importantly, our perceived performance improved dramatically. The page content arrives complete rather than skeleton → loading → content.

Bundle size: For content-heavy pages, RSC genuinely eliminates JS. Our product listing pages went from 180KB of JS to 32KB. The difference shows in performance on real user devices.

Colocation: Having the database query next to the component that uses it makes the code significantly easier to understand and maintain.

What Got Harder

State management across server/client boundary: You can’t pass non-serializable values from server to client components. No functions, no class instances, no React elements. This forces you to rethink how state flows through your application.

Debugging: When something goes wrong in a server component, the error appears in your server logs, not the browser console. Context switching between server and client debugging is friction.

Testing: Testing server components requires a fundamentally different setup than testing client components. React’s @testing-library/react doesn’t directly support async server components.

The Boundary Problem

The use client directive creates a boundary below which everything must be client code. This means if you need one small interactive element deep in a server-rendered tree, you have to lift the use client boundary up, potentially pulling large chunks of UI into client-rendered territory.

The pattern that works: extract interactive pieces into small, focused client components. Keep the data-fetching and composition logic in server components.

Recommendations

RSC works best when: you have data-fetching needs that map naturally to the component tree, your interactive elements are isolated and well-defined, and you’re starting fresh rather than migrating.

The migration story from class components or older Next.js patterns is genuinely painful. If you’re migrating, do it incrementally and expect the boundary between old and new patterns to be messy for a while.

For new projects: use RSC. The benefits are real. Just expect a 2-3 week ramp-up period for experienced React developers to internalize the mental model.

Related Reading