RSC and Mental Models - React on the Server
- KinfeMichael Tariku
379 peeps viewed
A New Paradigm
There's a lot to cover about the new mental models and architectures that React and Next.js have introduced. We'll dive deep into RSC (React Server Components) and address common points of confusion along the way.
The Good Old Days
Originally, React was designed to replace the traditional HTML + JS approach with something called JSX—a bundled, rich component system that makes websites interactive and responsive. This is what we call a frontend framework. Frameworks differ from libraries in that they provide a complete structure for building applications, whereas libraries solve specific, closely related tasks (you could call them modules, though they still have differences). But that's not the main point here—let's move on.
The traditional server-client model works, but we need better user experience at scale. Think of it like applying DRY (Don't Repeat Yourself) principles—no more unnecessary roundtrips from client to server. If you think of it like a recursive function, we're essentially applying Dynamic Programming concepts. This approach has us using client-side React APIs to handle everything, but it doesn't scale well.
So why not use something like a preprocessor—similar to C++ macros—where data can be stored and processed upfront, bringing the latest data to end users? I'm not saying this is the most efficient approach, but it's not new to us. Developers using PHP, Rails, and other server-side languages (not all, but many) have been using this pattern for years.
However, bringing this to life in a client framework is something cool—or perhaps challenging, depending on your perspective. The idea of processing data on the server for the UI layer is pretty compelling. The client side becomes straightforward: what you want to see is already there in React itself.
RSC - Scratching the Surface
Let's start by defining RSC: it's a new mental model for React that allows us to shift from a client-heavy approach to a hybrid or server-heavy model.
We all know that React is primarily a frontend framework—traditionally, it doesn't concern itself with servers, only bundlers (like ESBuild). But RSC changes this fundamental assumption.
RSC aims for zero-bundle-size React Server Components, which enable modern UX with a server-driven mental model. This is fundamentally different from SSR (more dynamic) and SSG (more static). We'll explore these differences as we go.
SSR vs SSG - The Foundation
Let's dive deeper. SSR, in more technical terms, is like turning JSX into an HTML string. This is the input and the byproduct in a symbiotic relationship.
When we break it down, SSR is a way to prepare and render your components on the server instead of the browser, sending the final product (HTML) to the browser. This can result in a fast First Contentful Paint (FCP) or Largest Contentful Paint (LCP).
The most simplified flow looks something like this: JSX → Server (runtime) → HTML → Client Browser.
With SSG, all pages are generated at build time as static pages (with some JavaScript tricks to load/preload content as fast as possible). Time-To-First-Byte (TTFB) is the best you can get, and you can host your website on a static CDN.
You can think of SSG as ideal for static websites where data can be processed at build time. The data is always available as props from the layout at a high level, and we can deploy it on a CDN. However, SSG can also work for dynamic content when there's data that's always up-to-date, allowing us to generate HTML from it.
It's the limitations of SSR that lead us to most of the concepts around RSC. Since JavaScript still needs to be fetched from a remote server to go through the hydration process, there's overhead. But what is hydration?
Hydration is the process of "bringing to life" a static HTML page by attaching JavaScript functionality to it. Think of it as painting interactivity onto your initially loaded page. You can break down the steps: hydration = download HTML → download JS → evaluate JS → attach event listeners → paint the state. This step needs to be done even if the page is SSR'd.
Some web frameworks implement resumability (like Qwik does)—serializing everything on pause and resuming anywhere inside your app. This is an optimization for hydration.
Enough of these concepts—let's get to RSC. As mentioned, it's something that makes React more than just a UI framework. It breaks down the traditional boundaries.
RSC enables migrating from client-first to server-first or hybrid architectures, allowing us to significantly reduce the bundle size shipped to the client, giving users a better UX.
So what happens is:
- The user opens their browser and requests to open the webpage.
- The server creates rendered content in HTML and sends it to the user.
- Meanwhile, static assets can be loaded from CDN, Edge, or dedicated servers.
- The user avoids client-server waterfalls—they can receive HTML immediately instead of being blocked waiting for everything to finish.
With SSR, website pages are generated at runtime on the server, which means it needs a runtime (like Node.js) to serve those requests. The contents are up-to-date, even though roundtrips still persist when you make a request, and SSR gives you the latest data from the server.
We can say that SSR provides faster load time (not build time), is ideal for dynamic sites, and is great for SEO since the content is already rendered and crawlers can index it. However, it comes with some cons: server costs for data-heavy apps, caching can be challenging, and Time-To-First-Byte is a bit slower because content is generated on the server for each request. You can mitigate this by adding a caching layer with a short TTL (Time To Live) to improve performance, since you can't deploy it on a static CDN.
So RSC and SSR are not the same thing, but they are complementary to each other. RSC enables rendering into an intermediate abstraction format without needing to add to the JavaScript bundle. This allows us to merge JSX tree nodes (the so-called server tree) with the client tree nodes without losing any kind of state and interactivity.
Server Components - The Powerhouses
These are components that render only on the server. They're the ones that will be streamed down to the client browser. They're also a replacement for SSR, but they create a really fast early paint and reduce the JS bundles that the client needs to download. This isn't meant to dismiss JavaScript—JS is still the most important part. Since you can't avoid it, at least we can optimize around it because great power comes with great bundle size.
If you're familiar with Next.js—it's a server-component-first framework (though calling it a "backend" framework is a bit of a stretch) solving a lot of modern web problems. You opt in using the use client directive to achieve more of a client-first approach.
Server components render before the client, so using server components in client components can cause a waterfall issue (the stuff that comes first—server at build time beforehand). When we say "clients," we mean anyone who wants to consume the server components—this could be browsers or other server origins. One thing to note: client components will be rendered once at compilation with undefined values, which is why you need to be careful about how you structure your component hierarchy.
Let's explore some core concepts, and we'll dive deeper in part 2.
When we talk about RSC, we usually have to talk about its backbone: Suspense and Streaming. Let's break them down.
Suspense is a big pipe architecture adoption. It's all about streaming, handling promises, streaming data, and presenting the UI as soon as it's available. It allows React to wait for asynchronous operations to complete while showing a fallback UI.
<Suspense fallback={<LoadingForAsyncComp />}>
<SomeAsyncComp />
</Suspense>
In the above example, we're streaming data from server to client. This is especially useful for operations that could take some time, like fetching from remote servers. Until everything gets resolved, we can ensure the fallback is displayed.
Streaming is like parallelism helping in popping the initial shells instead of blocking. Server-side HTML streaming and data fetching done in parallel is just an optimization. It's like moving from a blocking to a non-blocking model.
It's like sending the data bit by bit to the client.
Take a scenario where you display a gallery of 100 pictures on a paginated page. The naive way is to block everything until the data is available and show a spinner as a fallback, which isn't bad. But what if the images differ in size—having two extreme sizes, i.e., 20MB (the highest) and 10KB (the smallest)? Should you sacrifice the FCP or LCP for this size issue?
A better solution would be to make it size-agnostic: send each picture as soon as it's available and show a spinner, shimmer, or skeleton as a fallback for each image until it's loaded, instead of showing the same loading spinner for all while waiting for everything to resolve. Sending data bit by bit and constructing HTML on the fly is such a huge optimization that has been made so far.
Previously, when using React Suspense, components would wait until all the data and components in their subtree were ready before rendering anything. This could lead to delays in displaying content to the user, especially if there were asynchronous operations or network requests involved.
With the streaming feature, React components can start rendering and streaming their content to the client as soon as it's ready, regardless of the completion status of other components or data in the tree. This allows for a more progressive rendering experience, where the user sees partial content being rendered incrementally.
Streaming and Suspense work together well—for streaming content, you have to wrap those components that aren't loaded properly with Suspense and provide a fallback.
use client vs use server - Next.js Directives
It doesn't really mean client and server components in the traditional sense—it's not even like that.
use client means you are opting out of the server component (which means achieving the client component) from Next.js's default server-first component approach. You're telling the bundler—Turbopack—that you need to split this code into a separate client bundle so that it can be fetched easily once we know where we put our server code and client code. This creates a boundary between server and client code.
So whenever you need interactivity, you need to opt in by using the use client directive to enable client-first interactivity.
use server doesn't mean server components, but rather it's the bridge between your server code and client components. It's like code that you can invoke from the client on the server. For example, when a form is submitted, you can use use server to bridge the connection between client code and server code, like recording something in a database. These functions exclusively run on servers but are triggered from the client.
What's Next
- RSC Payloads
- ReactElement VS JSX
- In-depth guide on ReactDOM client and server APIs for parsing JSX syntax tree → HTML
- Suspense behind the scenes: ReadableStreams, promises, and SQS-like implementation
- Building a simple RSC framework—if I'm fortunate, with Nitro + Vite with @beka_cru on voice chat on my Telegram channel
- Strict rules on React 19: API-less vs bundler-heavy or strict rules
Much more is coming in this series.
Want to collaborate on this post? Hit me up on Telegram or fork the project and send a PR—I would love to see you here!