- React ecosystem
Where do React Server Components fit in the history of web development?
Intro
In this article I explain why React Server Components were introduced, in the light of the history of web applications development. Understanding where we come from is key to understand the challenges we currently face and the specific issues that Server Components aim to address.
ASP, PHP, what?
When I began developing websites in 1996, I created static HTML websites. These were just HTML files saved on a web server. When a browser sent a GET request, the web server would return the file, and the browser would parse it and display the page on the screen.
Then customers started requesting product catalogs or e-commerce systems. To accomplish this, you need a database and the capability to:
- Read from the database and generate HTML pages based on the database content.
- Write data from HTML forms into the database (for example, to update a product or place an order).
In the beginning, I used a technology called CGI to develop server applications written in Perl. This technology was later replaced by Microsoft’s ASP (Active Server Pages) and then PHP. PHP, which you may already know, is still powering over 77% of all the websites as of the time of writing (ever heard of WordPress?).
Interactivity happened essentially server-side: sending a request to the server and receiving a response. JavaScript was used for small interactions or animations, such as form validation, image change on hover, image carousels, etc.
The early days of SPA
With the increase of interactivity on websites and complex web applications, developers began to directly interact with the server from JavaScript using Ajax calls (the golden era of jQuery ;). For example, let's consider a form submitted by JavaScript. In this case, the server should no longer return an HTML page, but rather structured data intended to be read by the JavaScript script (such as just "ok" or "error").
As these techniques were used more often, a significant shift in paradigm emerged. This shift had a fundamental impact on both the client and server technologies, changing forever the way web applications were built.
-
Client-side, new frameworks emerged, to simplify the way HTML (the DOM) is updated in response to interactions or data from the server. The imperative approach of jQuery was not suitable for large client applications, so declarative frameworks were introduced to facilitate one-way or two-way data binding between data and DOM elements. I used Knockout.js for some time, then switched to Angular.js, and eventually settled on React.
-
Server-side, we witnessed the emergence of APIs, which no longer returned HTML pages but structured data. Initially data was XML sent over the SOAP protocol, and later JSON directly over HTTP, taking advantage of the REST architecture.
Applications created in this way are referred to as Single Page Applications, or SPAs.
This is because all paths are served by a single HTML page (e.g. index.html), where a JavaScript client-side router reads the browser location and renders the appropriate page.
Initially, Single Page Applications (SPAs) were primarily used for data-intensive web applications that required a login interface, such as the admin interface of an e-commerce or production management system. However, the architecture quickly became appealing for creating websites, as well, for several reasons:
- It allows for a clear separation of concerns between backend and frontend roles.
- From a security point of view, the attack surface of APIs is reduced and well-defined.
- When using a Node.js server, the JavaScript language can be used across the entire stack.
Problems of SPAs
Single Page Applications have 2 problems, especially for public websites.
1. SEO
As we saw, the web server returns just a skeleton or empty page, and the actual content is loaded by the JavaScript application. If a search engine's web crawler doesn’t execute JavaScript, it will just see an empty page without any content. This is not ideal for achieving a high ranking in search results, right?
2. Perceived performance
Before the user can see the page content, the following steps take place:
- The HTML page is fetched.
- The JavaScript file of the single-page application is fetched, parsed and executed.
- The SPA requests data from a server and waits for the response.
- The SPA renders the HTML based on the received data.
Meanwhile, the user is presented with a blank page or a spinner.
This can be problematic, especially in cases where the application is large, resulting in a longer loading time for the JavaScript file, or if the API server is slow to respond.
Server-Side Rendering to the Rescue
As we saw, Single Page Applications have two issues due to the fact that the page is generated on the client-side using JavaScript executed by the browser, without any content in the HTML sent by the server.
Server-side rendering is a solution to this problem.
If you have a server running Node.js, you can execute JavaScript code on the server. In particular, the server can fetch data, run the React code and render the HTML page. This allows users to immediately see the page as soon as it is fetched from the web server and enables search engines to correctly index the content.
How does it work? Instead of fetching data on the client, each React view (the top component of a route) can declare a static property called, for example, "getData", which is a function that retrieves the necessary data from APIs.
This function is called on the server side and the result is made available to the top component of the view. In this way the entire tree of components for the route can be rendered on the server using ReactDOMServer.renderToString().
I have used this approach before Next.js was created, for an e-commerce system that is still used by over 300 pharmacies in Italy. In Next.js, the equivalent function for “getData” is "getServerSideProps" (or "getStaticProps" for SSG, which will be discussed later).
You see the situation is very similar to what happens with a PHP page, but we are missing a piece: hydration.
Hydration
What we receive from the server is not just a static HTML page with no JavaScript and no interactivity. After the user receives the server-generated page, it continues to work as a Single Page Application, with client interactivity, client-based routing, and so on. How does this work?
Well, the script tag that points to the JavaScript bundle of our app is still present in the HTML code. As a result, it gets downloaded and executed by the browser. However, this time, React takes a different approach. After running the entire app and building the virtual DOM for a specific route, instead of directly writing it to the "root" div, React compares it with the server-generated DOM to ensure they are identical (have you ever seen hydration errors?). Then, React attaches the event handlers that enable interactivity to the DOM. This process is known as "hydration," as if the water of interactivity was being poured into the static HTML page.
If you have a global state manager like Redux (as I did in my e-commerce application) and need to initialize it with the server's status after rendering, from the server code you can inject a script into the HTML page. This script sets a global variable in the window object, where you can store the initial status, serialized as a string. When the app is loaded, it can read this status from the global variable and initialize the client's status (in my example, the Redux store).
A note about Static Site Generation (SSG)
Static Site Generation (SSG) is similar to server-side rendering (SSR), but instead of happening on the public web server that serves your website, it can occur on any machine running Node.js before publishing each new version of the website.
Think of it as having a person visit all the pages of your website on your development machine, saving the generated HTML pages, and then sending them via FTP to the actual web server. This web server can now be a “stupid” server or a content delivery network (CDN) that serves static files without the need of running Node.js.
Typically, the static generation process occurs on a cloud service (such as Netlify or Vercel) connected to your Git repository. Whenever there are code changes in the repository, the cloud service triggers a rebuild and serves the resulting build through a CDN.
Regardless of whether a page is pre-generated by SSG or generated on the fly by SSR, the client-side hydration process remains the same. From now on, when I mention SSR, I am referring to both SSR and SSG.
If you want to learn more about SSR vs SSG (and another concept called ISR), please read my article SSR vs SSG vs ISR. It was written in 2020, but the concepts are still valid.
Problems of Server-Side Rendering
1. Hydration
Reading the paragraph above about hydration, it is clear that the hydration process can be quite burdensome. It consumes time and resources, increasing the "Time-to-interactive" of your website, which negatively impacts page speed metrics. This is the main issue with Server-Side rendering.
Furthermore, when considering that the primary use case for SSR is not interactive web applications, but rather websites, it becomes apparent that the entire hydration process is largely unnecessary, since most components are non-interactive.
After the initial rendering, does a hero unit component really require interactivity or the execution of React code? Not at all, it could simply be plain static HTML!
Server components were designed to address this problem and eliminate the need for hydration when it is not required.
2. Useless bundle size on the client
Since many components are not interactive and should never re-render, sending to the browser the JavaScript code for these components is completely useless.
3. Fetching at component level
Another issue with SSR is that the root component of each route must declare its "getData" function and then make the data accessible to all components in the component tree. This limitation prevents us from fetching data in any component lower down in the tree, as it can only be done at the top level. While we can fetch data using an effect, this would only happen on the client-side, resulting in a loss of the SEO performance advantage provided by SSR.
React Server components
Server components are designed to prevent hydration when it is not necessary.
So, when is hydration necessary? Whenever we have interactions or the need to re-render. For an image carousel, for example, client-side React is required for the animation and user interaction, so the hydration is necessary.
Server Components are rendered just once, on the server, and they don’t have any JavaScript code that can be executed on the client. Therefore, we cannot use hooks like useState or useEffect in server components.
This is why, when adopting Server Components, it is important to determine which components can truly be Server Components and which ones should remain as “Client Components” (our old standard React components) with the client hydration.
By default, when using a RSC-compatible version of React (currently only available in canary releases), all components are treated as server components. However, if we require client interactivity, we can declare a component as a client component by including the use client
directive at the top of the file.
Server components will only render on the server, while client components will render on both the server and the client, during the hydration process. The code for server components is not included in the JavaScript bundle, as they will never be hydrated, re-rendered, or have any client interactivity.
So, we have two advantages that address the first two problems:
- No client hydration work is required for server components.
- The JavaScript bundle size is significantly reduced.
What’s more, we can directly fetch data from APIs in server components. This fetch occurs before the components are rendered server-side. In this way we also solve the third issue we encountered with Server-Side Rendering.
As of the time I am writing this, the recommended way to use Server Components is through the Next.js framework with the App Router.
If you are searching for a headless CMS solution that supports React Server Components, consider exploring React Bricks, co-founded by me, which recently released v4.2, fully supporting server components. It also provides two Next.js starter projects: one is a blank project, while the other one comes with Tailwind CSS, pre-made content blocks, and a blog.
Conclusion
In this post we discussed the problems that React Server Components are addressing in modern web development. We also took a time-machine trip way back in the web development history, to better understand where we come from. I believe that sometimes it’s important to view things from a broader perspective (or maybe it’s because I am getting older? ;).
Now we understand the benefits of React Server Components:
- They eliminate the need for client hydration for non-interactive components that don't require re-rendering.
- They reduce the JavaScript bundle size, as the JavaScript for Server components is not sent to the browser.
- They enable data fetching on the server directly from a single component, rather than just at the route level.
We did not go into the details of how server components are used, the composition rules for server and client components, or the server-only components that use the use server
directive to trigger Server Actions. But that’s material for another article 😊.