The "Server" in Server Components Is Mostly a Timing Detail

This blog is built with React Server Components (RSC). It is also served from a Cloudflare CDN on their free static hosting plan, which costs exactly zero dollars. Those two facts seem contradictory—if components are “server” components, how can the site be static?

The apparent paradox dissolves once you look at how modern frameworks actually work. The old division between “server” tools (Rails, PHP) and “static” site generators (Jekyll, Hugo) is increasingly obsolete. Any framework that follows the request/response model can produce a static site: just run the server during the build, make a request for every page you care about, and write the responses to disk. Doing that by hand would be tedious, which is why newer frameworks build the capability in directly.

Call these “hybrid” frameworks. They are conceptually server frameworks with an optional static output mode.

Why Hybrid Beats Static-Only

Specialization can be justified when it delivers something a general tool cannot. But that is not the case here. I cannot identify any advantage that a static-only tool offers developers or end users by virtue of being static-only. Static-only tools are not bad, but there is no reason to prefer them. There are, however, concrete reasons to prefer hybrid frameworks:

  • Less fragmentation. Two separate ecosystems for what is essentially the same code—the only difference is when it executes—is needless duplication.
  • More granular choices. You are not locked into one mode. The decision can be made route by route. Start fully static and add a dynamic page later, or start server-rendered and add static marketing pages. Projects that span both modes can share code and plugins.
  • A natural mental model. The request/response model is intuitive, and it covers both cases.

None of this is specific to RSC. Astro is not an RSC framework, yet it is hybrid: it outputs static sites by default while allowing you to opt into server features such as API routes and on-demand rendering. The same principle applies to RSC.

Walking the Walk

This blog is built with Next.js, which produces static sites by default. In fact, I enforce the constraint with the output: "export" option, which disables features requiring a server—because I do not run one. If I attempt to use any server feature, the static build fails, which is precisely what I want in this case.

With that setup, this React Server Component executes at build time during deployment:

export default async function Home() {
  const posts = await getPosts();
  return (
    <div className="relative -top-[10px] flex flex-col gap-8">
      {posts.map((post) => (
        <Link
          key={post.slug}
          className="block py-4 hover:scale-[1.005] will-change-transform"
          href={"/" + post.slug + "/"}
        >
          <article>
            <PostTitle post={post} />
            <PostMeta post={post} />
            <PostSubtitle post={post} />
          </article>
        </Link>
      ))}
    </div>
  );
}

The output appears on my homepage. The await getPosts() call reads from the filesystem—it is neither a client-side fetch nor server runtime code. It runs during deployment of a static blog.

Yes, calling something a “React Server Component” while running it statically is confusing. But if any server framework is already a static framework—just run it ahead of time and save the responses—the distinction stops mattering. Static is just a server that runs before anyone asks.