Logo
Published on

React Server Components

Authors

Why we should care about it?

Briefly - it is all about money - it's as simple as it can be. The impact of poor page load performance can lead to bad customer experience, customer with a poor internet connection or low-end CPU in their phone can eventually give up and leave your app.

What is the purpose of RSCs?

React Server Components aimed to:

  • minimize the network waterfall
  • increase performance by reducing JS bundle size, improving caching, increasing page load speeds
  • improve SEO

What are React Server Components?

React Server Components are application architecture that allows React to be rendered specifically on the server environment and after the result is streamed to the client.

What are the main benefits of RSCs?

Data Fetching:

  • RSCs are closer to the data source, a client sends fewer requests to the server to get the data it needs to render, due to this the React Server Components reduce the time and increase performance.

JavaScript's bundle size:

  • Small JavaScript bundles improve download speeds, and memory usage and reduce CPU cost. RSCs are rendered on the server with already populated data and you don’t need to send JS code to the client for those server components.

Largest Contentful Paint (LCP):

  • Server generates HTML that can be sent immediately to the client, without worrying about downloading, parsing, and executing JS.

Caching:

  • When RSCs fetch some data to generate a page this data can be cached on the server, and the server component itself can be cached between user's requests.

Rendering Patterns

In 2024 we can render content of our apps in many ways. The decision of how and where to fetch and render content is key to the performance of an application. The good balance is keeping a JS bundle as small as possible and delivering content using as few requests as possible.

Client-side Rendering (CSR)

Client-side rendering means rendering pages directly in the browser. On initial page load, a single HTML file generally has little to no content until the browser fetches the JavaScript and compiles everything. All logic, data fetching, and routing are handled on the client side. This means that more data is passed to the user's device from the server, and that comes with its own set of trade-offs. CSR is perfect for data-heavy dashboards, real-time apps, and apps that don't care about SEO.

Static Site Generation (SSG)

Static rendering happens at build-time, which means building a separate HTML page for each route ahead of time. SSG apps usually have a faster Largest Contentful Paint (LCP) compared to the CSR or SSR - because HTML page is already built while user requests it, and also a lower TBT and INP - assuming that the size of the JS bundle could be very small compared to other rendering patterns.

If you can pre-render a page ahead of a user's request or at least a bigger part of the page (the rest could be done using client-side JavaScript) - Static Site Generation (SSG) will be very good for your product.

Server-side Rendering (SSR)

SSR happens at request-time, Server-side Rendering allows you to transform React components into HTML and send it to the client. Server sends HTML page to the browser - browser renders HTML page - which is currently non-interactive, to make it interactive the client sends an additional request to fetch the JavaScript bundle to hydrate the components. Hydration is a technique in which client-side JavaScript converts a static HTML web page, delivered either through static hosting or server-side rendering, into a dynamic web page by attaching event handlers to the HTML elements.

Zero-Bundle-Size with React Server Components

React Server Components have zero impact on bundle size. You can use, any third-party packages without worrying about have they will impact bundle size of your app.

RSC vs CSR vs SSR

To understand better what React Server Components are and how they are compared with Client Side Rendering and Server Side Rendering let's consider the next topics and examples.

RSCs - are not single thing or feature, they are application architecture, that are meant to be used with meta frameworks, React team provided API, and frameworks will adopt it.

These examples demonstrate usage of the RSC, SSR, and CSR, and they were built to demonstrate:

  1. Differences between Data fetching patterns
  2. Differences between Bundle sizes of the apps

Tools that were used:

CSR, SSR, and RSC data fetching and rendering examples

Client-Side Rendering (CSR)

import { useEffect, useState } from "react";

const api = {
  fetchUsers(): Promise<DBUser[]> {
    // fetch, handle errors and return DBUser[] | null from API
  },
};

export default function MyComponent() {
  const [users, setUsers] = useState<User[]>([]);

  useEffect(() => {
    const fetchUsers = async () => {
      // Fetch users from api
      const result = await api.fetchUsers();

      // Sort users by name
      const sorted = result.sort((a, b) => a.name.localeCompare(b.name));

      // Replace dateOfBirth property with age
      const users = sorted.map((u) => {
        return {
          id: u.id,
          name: u.name,
          age: new Date().getFullYear() - new Date(u.dateOfBirth).getFullYear() - 1,
        };
      });

      // Set user object to state
      setUsers(users);
    };
    fetchUsers();
  }, []);

  return (
    <main className="main">
      <h3>Hello from client-side rendered components (interactive)</h3>
      {users.length > 0 && (
        <table>
          ... Render data here ...
        </table>
      )}
    </main>
  );
}

Server-Side Rendering (SSR)

const api = {
  fetchUsers(): Promise<DBUser[]> {
    // fetch, handle errors and return DBUser[] | null from API
  },
};

// This gets called on every request
export async function getServerSideProps() {
  // Fetch data from external API
  const result = await api.fetchUsers();

  // Pass data to the page via props
  return { props: { result } };
}

export default function MyComponent({ result }: { result: DBUser[] }) {
  // Sort users by name
  const sorted = result.sort((a, b) => a.name.localeCompare(b.name));

  // Replace dateOfBirth property with age
  const users = sorted.map((u) => {
    return {
      id: u.id,
      name: u.name,
      age: new Date().getFullYear() - new Date(u.dateOfBirth).getFullYear() - 1,
    };
  });

  return (
    <main className="main">
      <h3>Hello from server-side rendered components</h3>
      <table>
        ... Render data here ...
      </table>
    </main>
  );
}

React Server Components (RSCs)

export default async function MyComponent() {
  // Fetch data from DB
  const result = await db.users.findMany();

  // Sort users by name
  const sorted = result.sort((a, b) => a.name.localeCompare(b.name));

  // Replace dateOfBirth property with age
  const users = sorted.map((u) => {
    return {
      id: u.id,
      name: u.name,
      age: new Date().getFullYear() - new Date(u.dateOfBirth).getFullYear() - 1,
    };
  });

  return (
    <main className="main">
      <h3>Hello from react-server-components components (non-interactive)</h3>
      <table>
        ... Render data here ...
      </table>
    </main>
  );
}

CSR, SSR, and RSC bundle sizes examples

While using CSR or SSR bundle size tends to grow with the addition of new npm packages that need to be sent to the client. Let's add two heavy libraries for our app. Those libraries are added only for demonstration purposes, there may be smaller or larger and newer/faster alternatives.

// Add import statements
import moment from "moment";
import _ from "lodash";

...
// Sort users by name with the help of lodash
const sorted = _.sortBy(result, "name");          // new logic here

...
// Replace dateOfBirth property with age with the help of moment.js
const users = sorted.map((u) => {
  const m = moment(u.dateOfBirth, "MM-DD-YYYY");  // new logic here
  return {
    id: u.id,
    name: u.name,
    age: moment().diff(m, "years", false),        // new logic here
  };
});

Client-Side Rendering (CSR) with additional libraries⬇️

csr_with_libs

Client-Side Rendering (CSR) without libraries⬇️

csr_without_libs

Server-Side Rendering (SSR) with additional libraries⬇️

ssr_with_libs

Server-Side Rendering (SSR) without libraries⬇️

ssr_without_libs

React Server Components (RSCs) with additional libraries⬇️

rsc_with_libs

React Server Components (RSCs) without libraries⬇️

rsc_without_libs

You can found all the examples in: Github repo

For more information and examples, please check Github repo with: Next.js,zod, PostgresDB, drizzle or Prisma, or without ORM.

Conclusions

  1. Data fetching: As you may see data fetching techniques are very different. To fetch data using Client-Side Rendering components you must fire new requests to the API. To fetch data using React Server Components - you don't have to use components life-cycle methods or use getServerSideProps - you can just simply execute a function that fetches data directly from db, with the help of React.Suspense you can add loading states to your Server Components.
  2. Bundle size: As was described above, bundle size of the Client-Side Rendering app will grow when app grows, even with the help of tree-shaking and code splitting, you still gonna have additional code in your bundle, or you gonna have to make new requests to the server to fetch chunks of code.
  3. SEO: Better search engine crawling and indexing.

Cite Sources:

v8.dev/blog/cost-of-javascript-2019
github.com/reactwg/react-18/discussions/37
web.dev/articles/lcp
nextjs.org/docs/app/building-your-application/rendering
en.wikipedia.org/wiki/Hydration_(web_development)
plasmic.app/blog/how-react-server-components-work
blog.logrocket.com/deep-dive-react-fiber
react.dev/blog/2023/03/22/react-labs
web.dev/articles/rendering-on-the-web#static_rendering
patterns.dev
nextjs.org/docs/pages/building-your-application/rendering
nextjs.org/docs/app/building-your-application/rendering
nischithbm.medium.com/web
quickbooks-engineering
joshwcomeau.com/react/server-components
debugbear.com/docs/metrics