Over the past decade, React and its ecosystem has undergone continuous evolution. Each version has introduced new concepts, optimizations, and sometimes paradigm shifts, pushing the boundaries of what we thought was possible in web development.
React Server Components (RSC) is the latest and perhaps the most significant change since React hooks. Notably, big voices in the React community have endorsed RSCs, highlighting their importance in the future of React. However, this change has met with mixed reactions within the community.
For me, this line by Linkin Park captures the sentiment surrounding React’s evolution as we step into 2024:
Cause once you got a theory of how the thing works
Everybody wants the next thing to be just like the first
The new architecture is designed to support React Server Components, allowing them to run exclusively on the server and leverage server-side rendering benefits. But we've grown so accustomed to the React we know and love that embracing a paradigm shift, understandably, poses a challenge filled with hesitation and skepticism.
And while the concept of server and client components brings substantial improvements and flexibility to React development, it also introduces concepts that can be easily misunderstood.
The goal of this blog post is to guide you through the journey of React’s rendering evolution over the years and help you understand why React Server Components are not just inevitable but also the future of building cost-effective, high-performance React applications that deliver an exceptional user experience.
If you have been in the development game for a while, you'll remember React being the go-to library for creating Single Page Applications (SPAs).
In a typical SPA, when a client makes a request, the server sends a single HTML page to the browser (the client). This HTML page often contains just a simple div tag a reference to a JavaScript file. This JavaScript file contains everything your application needs to run, including the React library itself and your application code. It is downloaded when the HTML file is parsed.
The downloaded JavaScript code then generates the HTML on your computer and inserts it into the DOM under the root div element and you see the user interface in the browser.
This process is evident when you see the HTML appear in the DOM Inspector but not in the View Source option, which shows the HTML file sent by the server to the browser.
This method of rendering, where the component code is transformed into a user interface directly within the browser (the client), is known as client-side rendering (CSR).
Here’s the visualization of Client-side Rendering:
And here’s a look at the DOM inspector vs the page source for a React SPA:
CSR quickly became the standard for SPAs, with widespread adoption. However, it wasn't long before developers began noticing some inherent drawbacks to this approach.
First, generating HTML that mainly contains a single div tag is not optimal for SEO, as it provides little content for search engines to index. Large bundle size and a waterfall of network requests for API responses from deeply nested components may result in meaningful content not being rendered fast enough for a crawler to index it.
Second, having the browser (the client) handle all the work, such as fetching data, computing the UI, and making the HTML interactive, can slow things down. Users might see a blank screen or a loading spinner while the page loads. This issue tends to worsen over time as each new feature added to the application increases the size of the JavaScript bundle, prolonging the wait time for users to see the UI. This delay is particularly noticeable for users with slow internet connections.
CSR laid the groundwork for the interactive web applications we're used to today but to enhance SEO and performance, developers started looking for better solutions.
To overcome the drawbacks of CSR, modern React frameworks like Next.js, pivoted towards server-side solutions. This approach fundamentally changes how content is delivered to the user.
Instead of sending a nearly empty HTML file that depends on client-side JavaScript to construct the page, the server takes charge of rendering the full HTML. This fully-formed HTML document is then sent directly to the browser. Since the HTML is generated on the server, the browser is able to quickly parse and display it, improving the initial page load time.
Here’s the visualization of Server-side Rendering:
The server-side approach effectively resolves the issues associated with CSR.
First, it significantly improves SEO because search engines can easily index the server-rendered content.
Second, browsers can immediately load the page HTML content, instead of a blank screen or loading spinner.
SSR's approach to immediately improving the visibility of content has its own complexity, particularly when it comes to the page's interactivity. The full interactivity of the page is on hold until the JavaScript bundle — comprising React itself along with your application specific code — has been completely downloaded and executed by the browser.
This important phase, known as hydration, is where the static page, initially served by the server, is brought to life. During hydration, React takes control in the browser, reconstructing the component tree in memory based on the static HTML that was served. It carefully plans the placement of interactive elements within this tree.
Then, React proceeds to bind the necessary JavaScript logic to these elements. This involves initializing the application state, attaching event handlers for actions such as clicks and mouseovers, and setting up any other dynamic functionalities required for a fully interactive user experience.
Diving deeper, server-side solutions can be categorized into two strategies: Static Site Generation (SSG) and Server-side Rendering (SSR).
SSG occurs at build time, when the application is deployed on the server. This results in pages that are already rendered and ready to serve. It is ideal for content that doesn't change often, like blog posts.
SSR, on the other hand, renders pages on-demand in response to user requests. It is suitable for personalized content like social media feeds, where the HTML depends on the logged-in user. Usually, you'll see the two collectively being referred to as just server-side rendering or SSR.
Server-Side Rendering (SSR) was a significant improvement over Client-Side Rendering (CSR), providing faster initial page loads and better SEO. However, SSR introduces its own set of challenges.
One issue with SSR is that components cannot start rendering and then pause or "wait" while data is still being loaded. If a component needs to fetch data from a database or another source (like an API), this fetching must be completed before the server can begin rendering the page. This can delay the server's response time to the browser, as the server must finish collecting all necessary data before any part of the page can be sent to the client.
A second issue with SSR is that for successful hydration, where React adds interactivity to the server-rendered HTML, the component tree in the browser must exactly match the server-generated component tree. This means that all the JavaScript for the components must be loaded on the client before you can start hydrating any of them.
The third issue with SSR is related to hydration itself. React hydrates the component tree in a single pass, meaning once it starts hydrating, it won’t stop until it’s finished with the entire tree. As a consequence, all components must be hydrated before you can interact with any of them.
These three problems — having to load the data for the entire page, load the JavaScript for the entire page, and hydrate the entire page — create an all-or-nothing waterfall problem that spans from the server to the client, where each issue must be resolved before moving to the next one. This is inefficient if some parts of your app are slower than others, as is often the case in real-world apps.
Because of these limitations, the React team introduced a new and improved SSR architecture.
React 18 introduced Suspense for SSR to address the performance drawbacks of traditional SSR. This new architecture allows you to use the <Suspense>
component to unlock two major SSR features:
- HTML streaming on the server
- Selective hydration on the client
As we discussed in the previous section, traditionally, SSR has been an all-or-nothing affair. The server renders the complete HTML, which is then sent to the client. The client displays this HTML, and only after the complete JavaScript bundle is loaded does React proceed to hydrate the entire application to add interactivity.
Here’s the visualization of the above process:
However, with React 18, we have a new possibility. By wrapping a part of the page, such as the main content area, within the React Suspense
component, we instruct React it doesn’t need to wait for the main section data to be fetched to start streaming the HTML for the rest of the page. React will send a placeholder like a loading spinner instead of the complete content.
Once the server is ready with the data for the main section, React sends additional HTML through the ongoing stream, accompanied by an inline <script>
tag containing the minimal JavaScript needed to correctly position that HTML. As a result of this, even before the full React library is loaded on the client side, the HTML for the main section becomes visible to the user.
Here’s the visualization of HTML streaming with <Suspense>
:
This solves our first problem. You don’t have to fetch everything before you can show anything. If a particular section delays the initial HTML, it can be seamlessly integrated into the stream later. This is the essence of how <Suspense>
facilitates server-side HTML streaming.
While we can now speed up the initial HTML delivery, we still have another challenge. Until the JavaScript for the main section is loaded, client-side app hydration cannot start. And if the JavaScript bundle for the main section is large, this could significantly delay the process.
To mitigate this, code splitting can be used. Code splitting means you can mark specific code segments as not immediately necessary for loading, signaling your bundler to segregate them into separate <script>
tags.
Using React.lazy
for code splitting enables you to separate the main section's code from the primary JavaScript bundle. As a result, the JavaScript containing React and the code for the entire application, excluding the main section, can now be downloaded independently by the client, without having to wait for the main section's code.
This is crucial because by wrapping the main section within <Suspense>
, you've indicated to React that it should not prevent the rest of the page from not just streaming but also from hydrating. This feature, called selective hydration allows for the hydration of sections as they become available, before the rest of the HTML and the JavaScript code are fully downloaded.
From the user’s perspective, initially they get non-interactive content that streams in as HTML. Then you tell React to hydrate. The JavaScript code for the main section isn’t there yet, but it’s okay as we can selectively hydrate other components.
The main section is hydrated once its code is loaded.
Thanks to selective hydration, a heavy piece of JS doesn’t prevent the rest of the page from becoming interactive.
Here’s the visualization of selective hydration with <Suspense>
:
Moreover, selective hydration offers a solution to a third issue: the necessity to "hydrate everything to interact with anything". React begins hydrating as soon as possible, enabling interactions with elements like the header and side navigation without waiting for the main content to be hydrated. This process is managed automatically by React.
In scenarios where multiple components are awaiting hydration, React prioritizes hydration based on user interactions. For instance, if the sidebar is about to be hydrated and you click on the main content area, React will synchronously hydrate the clicked component during the capture phase of the click event. This ensures the component is ready to respond immediately to user interactions. The sidenav
is hydrated later on.
Here’s the visualization of hydration based on user interactions:
First, even though JavaScript code is streamed to the browser asynchronously, eventually, the entire code for a webpage must be downloaded by the user. As applications add more features, the amount of code users need to download also grows. This leads to an important question: should users really have to download so much data?
Second, the current approach requires that all React components undergo hydration on the client-side, irrespective of their actual need for interactivity. This process can inefficiently spend resources and extend the loading times and time to interactivity for users, as their devices need to process and render components that might not even require client-side interaction. This leads to another question: should all components be hydrated, even those that don't need client side interactivity?
Third, in spite of servers' superior capacity for handling intensive processing tasks, the bulk of JavaScript execution still takes place on the user's device. This can slow down the performance, especially on devices that are not very powerful. This leads to another important question: should so much of the work be done on the user's device?
To address these challenges, simply taking an incremental step is not enough. We need a significant leap towards a more powerful solution.
React Server Components (RSC) represent a new architecture designed by the React team. This approach aims to leverage the strengths of both server and client environments, optimizing for efficiency, load times, and interactivity.
The architecture introduces a dual-component model, differentiating between Client Components and Server Components. This distinction is not based on the functionality of the components but rather on where they execute and the specific environments they are designed to interact with. Let's take a closer look at these two types:
Client Components are the familiar React components we've been using and talking about in the previous rendering techniques. They are typically rendered on the client-side (CSR) but, they can also be rendered to HTML once on the server (SSR), allowing users to immediately see the page's pre-rendered HTML content rather than a blank screen.
The idea of "client components" rendering on the server might seem confusing but it's helpful to view them as components that primarily run on the client but can (and should) also be executed once on the server as an optimization strategy.
Client components have access to the client environment, such as the browser, allowing them to use state, effects, and event listeners, to handle interactivity and also access browser-exclusive APIs like geolocation or localStorage, allowing you to build the frontend for specific use cases, just as we've done all these years before the introduction of the RSC architecture.
In fact, the term client component doesn’t signify anything new; it simply helps differentiate these components from the newly introduced Server Components.
Here’s an example of a Counter
client component:
"use client"
export default function Counter() {
const [count, setCount] = useState(0);
return (
<div>
<h2>Counter</h2>
<p>{count}</p>
<button onClick={() => setCount(count + 1)}>Increment</button>
</div>
);
}
Server Components represent a new type of React component specifically designed to operate exclusively on the server. And unlike client components, their code stays on the server and is never downloaded to the client. This design choice offers multiple benefits to React apps. Let's take a closer look at these benefits.
Zero-bundle sizes
First, in terms of bundle sizes, Server Components do not send code to the client, allowing large dependencies to remain server-side. This benefits users with slower internet connections or less capable devices by eliminating the need to download, parse, and execute JavaScript for these components. Additionally, it removes the hydration step, speeding up app loading and user interaction.
Direct access to server-side resources
Second, by having direct backend access to server-side resources like databases or file systems, Server Components enable efficient data fetching and rendering without needing additional client-side processing. Leveraging both the server's computational power and proximity to the data source, they manage compute-intensive rendering tasks and send only interactive pieces of code to the client.
Enhanced security
Third, Server Components' exclusive server-side execution enhances security by keeping sensitive data and logic, including tokens and API keys, away from the client-side.
Improved data fetching
Fourth, Server Components enhance data fetching efficiency. Typically, when fetching data on the client-side using useEffect
, a child component cannot begin loading its data until the parent component has finished loading its own data. This sequential fetching of data often leads to poor performance.
The main issue is not the round trips themselves, but that these round trips are made from the client to the server. Server Components enable applications to shift these sequential round trips to the server side. By moving this logic to the server, request latency is reduced, and overall performance is improved, eliminating client-server waterfalls.
Caching
Fifth, rendering on the server enables caching of the results, which can be reused in subsequent requests and across different users. This approach can significantly improve performance and reduce costs by minimizing the amount of rendering and data fetching required for each request. This is particularly useful for static data that doesn't change frequently.
Faster initial page load and First Contentful Paint
Sixth, Initial Page Load and First Contentful Paint (FCP) are significantly improved with Server Components. By generating HTML on the server, pages render immediately without the delay of downloading, parsing, and executing JavaScript. This is especially beneficial for marketing site pages that don't require much interactivity.
Improved SEO
Seventh, regarding Search Engine Optimization (SEO), the server-rendered HTML is fully accessible to search engine bots, enhancing the indexability of your pages.
Efficient streaming
Lastly, there's streaming. Server Components allows the rendering process to be divided into manageable chunks, which are then streamed to the client as soon as they are ready. This approach allows users to start seeing parts of the page earlier, eliminating the need to wait for the entire page to finish rendering on the server.
Here’s an example of a ProductList page server component. This async component demonstrates how server components run and fetch data directly on the server:
export default async function ProductList() {
const res = await fetch("https://api.example.com/products");
const products = res.json();
return (
<main>
<h1>Products</h1>
{products.length > 0 ? (
<ul>
{products.map((product) => (
<li key={product.id}>
{product.name} - ${product.price}
</li>
))}
</ul>
) : (
<p>No products found.</p>
)}
</main>
);
}
The introduction of Server Components marks a significant shift in the React ecosystem, offering new possibilities for building efficient and performant applications. As the React docs explain, this new rendering strategy combines the best aspects of server side React and client side React, allowing developers to create more optimized applications using familiar React features.
In the React Server Components paradigm, it's important to note that by default, every component in a Next.js app is considered a Server Component.
To define Client Components, we must include a directive — in other words, a special instruction — at the top of the file: "use client"
. This directive acts as our ticket to cross the boundary from server to client side and is what allows us to define client components.
It signals to the bundler that this component, along with any components it imports, is intended for client-side execution. As a result, the component gains full access to browser APIs and the ability to handle interactivity.
The “use server” directive marks server-side functions that can be called from client-side code. We will cover “use server” and server actions in a separate post.
Let’s explore RSC rendering lifecycle assuming Next.js as the React framework.
Vercel with Next.js 13 App Router was the first to support the React Server Components (RSC) architecture.
For React Server Components (RSC), it's important to consider three elements: your browser (the client), and on the server side, Next.js (the framework) and React (the library).
Initial loading sequence
- When your browser requests a page, the Next.js app router matches the requested URL to a Server Component. Next.js then instructs React to render that server component.
- React renders the server component and any child components that are also Server Components, converting them into a special JSON format known as the RSC payload. If any Server Component suspends, React pauses rendering of that subtree and sends a placeholder value instead.
- Meanwhile, client components are prepared with instructions for later in the lifecycle.
- Next.js uses the RSC Payload and Client Component JavaScript instructions to generate HTML on the server. This HTML is streamed to your browser to immediately show a fast, non-interactive preview of the route.
- Alongside, Next.js streams the RSC payload as React renders each unit of UI.
- In the browser, Next.js processes the streamed React response. React uses the RSC payload and client component instructions to progressively render the UI.
- Once all Client Components and the Server Components' output has been loaded, the final UI state is presented to the user.
- Client components undergo hydration, transforming our app from a static display into an interactive experience.
This is the initial loading sequence. Next, let's look at the update sequence for refreshing parts of the app.
Update sequence
- The browser requests a refetch of a specific UI, such as a full route.
- Next.js processes the request and matches it to the requested Server Component. Next.js instructs React to render the component tree. React renders the components, similar to the initial loading.
- But, unlike the initial sequence, there's no HTML generation for updates. Next.js progressively streams the response data back to the client.
- On receiving the streamed response, Next.js triggers a re-render of the route using the new output.
- React reconciles (merges) the new rendered output with the existing components on screen. Since the UI description is a special JSON format and not HTML, React can update the DOM while preserving crucial UI states, such as focus or input values.
This is the essence of the RSC rendering lifecycle with the App Router in Next.js.
With React Server Components architecture, Server Components take charge of data fetching and static rendering, while Client Components are tasked with rendering the interactive elements of the application.
The bottom line is that the RSC architecture enables React applications to leverage the best aspects of both server and client rendering, all while using a single language, a single framework, and a cohesive set of APIs. RSCs improve upon traditional rendering techniques while also overcoming their limitations.
For more context and and a more comprehensive mental model on RSCs, refer to the Next.js docs or watch my Next.js tutorial on YouTube.
Introducing Visual Copilot: convert Figma designs to high quality code in a single click.