An In-Depth Look at React Server Components!

Lately, React Server components (also known as Server components or RSC) have been creating considerable excitement and interest. Their popularity has surged further as they were recently adopted as the default option in Next.js 13, leading to a growing number of developers embracing them in their projects.

Before we delve into the concept and rising popularity of Server components, let’s take a moment to explore the factors that prompted the introduction of Server components.

Understanding the Need for Server Components

Initially, React started as a small library for creating interactive websites. But as its popularity soared, developers began using it for more complex web applications. Unfortunately, this transition to larger applications led to inherent issues and challenges.

The problems faced in web applications

1. Large bundle size

One problem is that React applications are typically bundled with all of the JavaScript they need to function, even if the client doesn’t need all of that code. This can lead to large JavaScript bundles that take a long time to download and execute. Although there are some techniques(tree-shaking, minification, etc) to reduce this bundle size, a significant amount of JS is still shipped.

// Example from React RFC
// NOTE: *before* Server Components a total of 240K (uncompressed) code is shipped

import marked from 'marked'; // 35.9K (11.2K gzipped)
import sanitizeHtml from 'sanitize-html'; // 206K (63.3K gzipped)

function NoteWithMarkdown({text}) {
  const html = sanitizeHtml(marked(text));
  return (/* render */);
}

2. Longer load time

As React was initially intended for client-side use, users had to download and execute substantial JavaScript before anything appeared on the screen. This was generally manageable for smaller apps, but it became problematic for larger ones, leading to significant delays in application interaction. Consequently, the user experience suffered, and businesses risked losing customers.

Although code splitting can help to improve performance to some extent, developers usually forget to replace regular import statements with React.lazy and dynamic imports.

3. Client-Server waterfalls

The performance of applications can suffer when they make consecutive requests to fetch data. For instance, consider a scenario where Component CountryDetails contains Child Component Region. Each component is responsible for fetching its own data, but Region cannot start fetching data until CountryDetails completes. This sequential dependency can lead to delays in data fetching and potentially hinder the overall performance of the application.

export const CountryDetails = ({ countryName }) => {
  const [countryId, setCountryId] = useState(null)
  useEffect(async () => {
    const countryData = await fetchCountryData(countryName);
    setCountryId(countryData.id)
  });
  render(<Region id={countryId} />)
}
export const Region = ({ id }) => {
  const [regionDetails, setRegionDetails] = useState(null)
  useEffect(async () => {
    const data = await fetchRegionDetails(id);
    setRegionDetails(data)
  });
  <div>{regionDetails.name}</div>
}

export default function App() {
  return (<CountryDetails countryName='india' />);
}

These issues arise because React is primarily client-centric. To tackle these problems, frameworks like Next.js and Gatsby introduced server-side rendering (SSR). However, SSR came with its own challenges, as the rendered HTML still needs to be hydrated on the client, which can introduce latency.

Recognizing the limitations of pure server rendering and pure client rendering, React introduced server components.

Server components allow apps to leverage the advantages of both server and client rendering, providing a more effective solution.

Let’s check out what Server components are in detail.

What are Server components?

Server components are specialized components that execute on the server side. Their architecture bears similarities to PHP/Rails sites, where React operates on the client side, creating a combination of server and client modes. Essentially, server components extend the React model, enabling us to remain within a single framework, use one programming language, and adhere to one paradigm. This approach offers a cohesive and unified development experience.

Features of Server components

  1. Zero impact on bundle size: Server components have no impact on the overall bundle size, allowing for efficient loading and rendering of components.
  2. Access to server-side data sources: Server components can access server-side data sources, such as databases, file systems, or (micro)services. This allows Server components to render dynamic content that is based on data that is stored on the server.
  3. Seamless integration with client components: They can load data on the server and pass it as props to client components, allowing the client to handle rendering the interactive parts of a page.
  4. Dynamic rendering: Server Components can dynamically choose which client components to render, allowing clients to download just the minimal amount of code necessary to render a page.
  5. State preservation: Server components preserve the client state when reloaded. This means that the client state, focus, and even ongoing animations are not disrupted or reset when a Server components tree is re-fetched. This can improve the user experience, especially for pages that require a lot of user input.
  6. Progressive rendering: Server components are rendered progressively and incrementally stream rendered units of the UI to the client. This allows developers to craft intentional loading states and quickly show important content while waiting for the remainder of a page to load.

Now, let’s examine how server components can effectively address the issues we previously discussed in this blog.

How Server Components Solve the Problems of Web Applications?

1. Reducing bundle size:

Server components run exclusively on the server. This means that the code for the server components is never sent to the client. Instead, the server renders the components and generates the resulting HTML, which is then sent to the client. This approach offloads much of the computational load to the server, thereby enhancing the performance of the React application and ensuring faster operation.

  // Example from React RfC
  // Server Component === zero bundle size
  // A code savings of over 240K (uncompressed):
  
  import marked from 'marked'; // zero bundle size
  import sanitizeHtml from 'sanitize-html'; // zero bundle size
  
  function NoteWithMarkdown({text}) {
    // same as before
  }

2. Improving load time:

Server components offer automatic bundle splitting, eliminating the need for manual dynamic imports. As a result, the server sends only the components required for a specific page, significantly enhancing page performance, particularly for users with slower internet connections. This optimization is a beneficial outcome of the server component architecture, where there exists a symbiotic relationship between the server and the client.

3. Resolving the waterfall modal issue:

Let’s revisit the example of the waterfall model we discussed earlier. The main issue affecting the performance of the application is the high latency between the client and the server.

To address this issue, we relocate components CountryDetails and Region from the client to the server. When the client makes the request, the parent component resides on the server, enabling low-latency access to data as it is closer to the data center. Consequently, communication with the backend occurs swiftly. Subsequently, the child component is rendered, also benefiting from fast communication with the backend and promptly sending the response.

Server components architecture

In Server components architecture, we encounter three types of components:

1. Server Components:

These components operate on the server and are positioned in close proximity to the data source. When dealing with code responsible for data fetching and pre-fetching, it is logical to place these components on the server. Typically, such components are named with the “.server.js” extension, for instance, “ServerComponent.server.js.”

2. Client Components:

Client Components function within the browser environment. When there is a need for code to respond swiftly to user interactions and provide immediate feedback, it is appropriate to place such code on the client side.

Client Components are identified by a use client directive at the top of the file. However, it’s worth noting that use client does not have to be included in each client file that utilizes client-specific features. It serves as the entry point from the server to the client realm, and anything imported from that point functions as normal React code.

Naming conventions for Client Components involve using the “.client.js” extension, such as “ClientComponent.client.js.”

3. Shared Components:

Shared components have the capability to be rendered on both the server and the client. The decision of whether a shared component renders on the server or the client is contingent on the type of component that imports it.

Naming conventions for Shared Components involve using the “.js” extension, for example, “SharedComponent.client.js.” This allows for flexibility in utilizing the same component across both server and client environments.

As a general rule, it is advisable to prefer Server components over client components unless there are specific reasons necessitating the use of client components. This preference is also reflected in Next.js 13, which defaults to Server components as a best practice.

The diagram shows that we can mix and match server and client components. In this conceptual model, the emphasis is less on client components fetching data and more on Server components passing the data to the Client components as props.

To comprehend this conceptual model, consider the metaphor Dan Abramov explains in the JS Party podcast. He compares server components to a skeleton, representing a basic structure consisting of components like the header, footer, content area, and main bar. Initially, this skeleton has limited functionality.

However, as the application evolves, specific enhancements are desired, such as transforming the main bar into a tab switcher. To achieve this, we augment the skeleton by wrapping some “muscles” around it. These muscles correspond to client components, which provide progressive enhancement and interactivity to the application.

In summary, the server components form the core structure, acting as the skeleton, while client components act as the muscles that add functionality and interactivity, dynamically enhancing the application as it evolves.

How do Server components work?

Server components require the support of a framework like Next.js or Hydrogen for routing and bundling. Let’s use the Server components demo app to gain insight into how server components function. The demo app incorporates a webpack bundler plugin for routing purposes.

Here’s a step-by-step overview of how server components work in the demo app:

On the server side:

  1. The framework’s router matches the requested URL to a Server Component and passes route parameters as props to the component.

  2. React renders the root Server Component and any child Server Components.

  3. The rendering process halts at native components (e.g., divs, spans) and Client Components.

  4. Native components are streamed as a JSON description of the UI. For example, in the App.server.js file, a native component is streamed as shown below:

  'J0':["$","div",null,{"className":"main","children":[["$","section",null,{"className":"col sidebar","children":[["$","section",null, ...]]}]]}]
  

Client Components are also streamed, including serialized props and a bundle reference to the component’s code. For instance, the SearchField.client.js file is streamed as follows:

  M1:{"id":"./src/SearchField.client.js","chunks":["client5"],"name":""}
  

The framework progressively streams the rendered output to the client as React renders each unit of UI. By default, React returns a description of the rendered UI, not HTML. This is necessary to allow reconciling (merging) newly fetched data with existing Client Components.

On the client side,

  1. The framework receives the streamed React response and renders it on the page with React.

  2. React deserializes the response and progressively renders the native elements and Client Components.

  3. Once all Client Components and Server Components’ output have loaded, the final UI state is displayed to the user.

Although Server Components offer powerful capabilities, they do come with certain limitations.

Limitations of Server Components

  1. Cannot use hooks provided by React like useState, useReducer, useEffect, and so on, as server components are rendered on the server.
  2. Cannot use browser APIs like Local Storage
  3. Cannot use custom hooks that depend on state or effects.

When to use Server components?

We should use Server Components as long as we do not have a reason to use Client components. That’s also why Next.js defaults to Server components.

When to use Client components?

  • When the components need to be interactive, for instance, utilizing onClick or onChange listeners on DOM elements.
  • When there is a requirement to utilize browser-specific Web APIs.
  • When accessing the user’s real location through the geolocation Web API or an IP Geolocation API.
  • When relying on data stored in the browser, such as LocalStorage.
  • When the component necessitates the use of lifecycle hooks or internal state management, such as useState, useReducer, etc

Will Server components replace Server-side rendering (SSR)?

No, Server components are not intended to replace Server-side rendering (SSR). Instead, they can complement SSR to enhance the application’s overall performance.

Lauren Tan provides a clear explanation of the difference between these two approaches and how they can be utilized together in her Twitter thread.

How Server components and SSR can work together to improve the application performance?

By employing Server Components, we can first render the UI into an intermediate format and then utilize the SSR infrastructure to render it into HTML, ensuring a fast startup time. In this setup, the JS bundle sent to the client will be much smaller, contributing to improved performance.

That concludes this post. Hope it was useful!

We have curated a list of essential references regarding Server components -

Need help on your Ruby on Rails or React project?

Join Our Newsletter