Skip to main content

Rendering and Component Types in Next.js - CSR, SSR, SSG, and ISR

A detailed article on different rendering methods and component types in Next.js

Updated 14 min readMarkdown
Part of seriesNext JS

Rendering methods are one of the most confusing parts of Next.js for newer developers, especially those who’ve recently moved over from React.

That's because React has just one way of rendering and working with components that is Client Side Rendering or CSR. So, developers working in pure React always deal with CSR and do not have to worry about anything, for rendering methods at least.

But Next.js has a totally different ecosystem on this. In fact, one of the reasons why Next got so popular is that it solves a big problem with apps based on CSR, and that's SEO.

In Next.js, you will get various types of rendering methods, each serving a different purpose; no one wrong, no one right, just what suits the purpose and type of your app works best for you.

How Pure SPAs Work and The Problem With Them

A pure React app (without a framework like Next), is generally based on client-side rendering.

That means the client has full control over the app, the routing or navigation, loading the scripts, running data fetches and all.

Here's how such an app, or what we call as a classic SPA works.

The client makes an initial request, requests the app from the server, on route (/).

The server sends a minimal HTML shell, with a script file, something that looks like this

<!doctype html>
<html>
  <head>
    <link rel="stylesheet" href="/assets/index-a1b2c3.css">
    <link rel="modulepreload" href="/assets/vendor-9f8e7d.js">
  </head>
  <body>
    <div id="root"></div>
    <script type="module" src="/assets/index-4d5e6f.js"></script>
  </body>
</html>

You can see, it is just a minimal HTML layout with a script file linked.

Now, from this script, the client fetches the whole app and components, the routing or navigation logic encoded and after this, the client has to fetch the data from the server if any of the component mounted does so.

After this, we have a huge advantage: the client has the whole app's logic, the routers and navigation too (that's why we use React Router while working with SPAs). Hence, while navigating to another page or route, the client does not have to make a request to the server again (since everything is already on the client), and the navigations feel instant.

But we do have a big problem too with this. If you notice the whole process again, it looks like:

  1. Client requests app from the server.

  2. Server sends HTML with a script.

  3. Client fetches components from the script to render on the browser.

  4. Client hydrates the components. Everything becomes Interactive.

  5. If a component fetches data from the server, the client has to fetch the data from the server. That's an extra round trip to the server.

Until step 3, the user is staring at a blank screen, and only after step 4 is completed user can interact with the components (click buttons, navigate to links, etc.), and only after step 5 is done, user finally sees the data requested from the server.

So while the client gets full control once the app has loaded, the user sees almost nothing useful until all of those requests have finished.

That's terrible for SEO purposes. Google does execute JavaScript, but later and less reliably, and many other crawlers and link-preview bots don’t run it at all. So CSR pages are slower and less dependable to index.

In an application powered by pure CSRs, all these steps happen in waterfall:

  1. Initial HTML Shell load.

  2. Client fetches JS from bundles and hydrates components.

  3. Data fetching from those components.

And all of them block each other.

And that is the biggest downgrade of CSRs, and all the other rendering methods in Next.js exist to solve this, one way or the other.

How Server Side Rendering (SSR) Works

The main problem with Client Side Rendering was its waterfall behaviour of loading components or data. Also, it has to make a round trip to the server to fetch the data once the components are mounted and hydrated by the client.

SSR solves this with the idea of rendering components on the server (fetching data from the server becomes easy) and then giving the complete ready component to the client instead of giving it scripts to fetch the data from the server.

In SSR, since components are rendered on the server, you have an option to make the data fetch directly in the component, instead of making a round trip to the server after rendering the component like CSR.

Suppose you have an about page that fetches data via some API and then shows it on the browser (client).

It may look something like this in a classic CSR app:

export default function AboutPage() {
    const [data, setData] = useState(null);
    
    useEffect(() => {
    ;(
        async() => {
            const response = 
            await fetch(`http://mysite.com/founderInfo`);
            const data = await response.json();
            setData(data);
        }
    )()
    }, [])


    return (
         <div className="flex min-h-screen justify-center">
            <h2>Founder's Info</h2>
            <span>Here's about the founder of our platform</span>
            <p>{data?.founderInfo}</p>
        </div>
    );
}

What happens above is:

The client loads components from the JS bundles or scripts fetched from the server, hydrates those components (including this About Page component) and then, through this component, the client fetches data from the server.

The same data fetch call in SSR may look like:

export default async function AboutPage() {
    const response = 
    await fetch(`http://mysite.com/founderInfo`);
    
    const data = await response.json();
    const founderInfo = data.founderInfo;

    return (
        <div className="flex min-h-screen justify-center">
            <h2>Founder's Info</h2>
            <span>Here's about the founder of our platform</span>
            <p>{founderInfo}</p>
        </div>
    );
}

You see here, since the component is rendered on the server itself, there is no need for the client to make an extra api call to the server to get the data.

And it is not just about API calls, any async operations can happen directly inside these components, meaning if the data is in your local DB only, you can also do

export default async function AboutPage() {
    const data = await db.get(); // your db call directly here
    const founderInfo = data.founderInfo;

    return (
        <div className="flex min-h-screen justify-center">
            <h2>Founder's Info</h2>
            <span>Here's about the founder of our platform</span>
            <p>{founderInfo}</p>
        </div>
    );
}

What happens here with SSRs is:

Client makes the request to the server to get the component. The server renders the component, takes up the database calls, fetches the data and makes the component ready and serves to the client.

So at last, the client gets a fully data-ready component, it does not have to make a round trip to the server to fetch the data.

SSR v/s RSC

Although many developers use Server Side Rendering (SSR) and React Server Components (RSC) interchangeably, they are two different things altogether.

SSR is a rendering method, while RSC is a component type in React.

What happens in SSR, as we discussed above, is that the components get rendered on the server. Now these components may need to hydrate (attaching listeners, navigation, etc.), which can happen only on the client.

This means, it is possible that a component is rendered on the server (via SSR), and some part of it needs to render on the client also for hydration (Button onClick, hooks, navigations, etc.).

For such server-side rendered components, we also need to re-render them on the client. Here's when two different types of components come out:

  1. Server Components

  2. Client Components

Server Components are those which only need to render on server, they do not need to hydrate at all, so re-rendering them on client is not required.

By default, a component in Next.js is a server component only. And you will see, you can't use hooks, handlers, or navigation in those components, as all of them require hydration and re-rendering on client after server, but server components must be rendered on the server only.

On the other hand, there are client components, when used with SSR, these components render on the server (as per the rule of SSR), and they re-render on the client also for hydration.

To make a component a client component in Next, you need to add the "use client" directive at the top, and now you can use the hooks, handlers and everything here.

So, rendering and component types are two different things. SSR is a rendering method, and RSC is a component type. SSR can be implemented with both RSC (React Server Component) and RCC (React Client Component).

Why Client Components need to render on Server First

Now, one can argue that what's the need of rendering client components on the server first, as we anyway need to render them on the client later, so why don't we render them on the client directly?

The idea is good, and there is an option in Next.js to disable SSR for client components explicitly, but this way we will again hit the same problem with traditional CSR. If the server wouldn't handle the initial HTML load for client components, the whole page would arrive blank, and we would revert to traditional CSR rendering.

So, just remember:

  • Components in Next.js (server or client) render on the server by default.

  • The Client Components just re-render on the client too after the server for hydration purposes.

  • Client Components run on the server once (initial HTML load) and then on the client (for hydration and every other render after that).

  • We can explicitly disable the SSR for client components, but that way we will hit the traditional CSR drawback again.

What is Static Site Generation (SSG)

Now, since we are clear with CSR and SSR, SSG is way easier to understand.

SSG is nothing but a type of SSR itself.

Since SSR means rendering the components on the server, this could be done in two possible ways:

  1. Rendering the component on the server only once at build time and serving each user the same component.

  2. Rendering the component on the server per request.

Rendering components on the server could be done either way.

There could be a server component, whose content will not change, no matter who requests it and how many times.

Like if the about page of your app always serves the same content, irrespective of who makes the request.

// src/app/about/page.tsx

export default async function AboutPage() {
    return (
        <div>
             <h2>Our Team</h2>
             <ul>
                <li>Member 1</li>
                <li>Member 2</li>
                <li>Member 3</li>
            </ul>
        </div>
    );
}

It sounds great to render it on the server once, cache it into the CDN and serve from the CDN to the users, instead of re-rendering the component. Since the content of the component is going to be the same always, it makes no sense to re-render it on the server again and again, on every request.

And when you decide to change the content, rebuild the component, update it to the CDN and then you will have your component serving the updated content.

That's exactly how SSG works, and as the name suggests, it is Static Site Generation, the content is Static, it remains the same over requests.

But component content may change per request too.

Suppose there is a profile component, which fetches the User Profile from the database and displays the info.

// src/app/profile/page.tsx

export default async function ProfilePage() {
    const session = await auth()

    // your db call:
    const data = await db.getProfile(session.user.id);
    const userInfo = data.userInfo;

    return (
        <div>
             <h2>User Info</h2>
             <h3>username: {userInfo.username}</h3>
             <p>More Info: {userInfo.moreInfo}</p>
        </div>
    );
}

Now this component needs to show the data depending on who is requesting it. The data to be displayed depends on the user that is logged in and calling for the component.

So, the data depends on who is requesting the component and may change from request to request. That's where we can't render a component just once on the server, build it statically and serve it from the CDN to all.

This component needs to be rendered on every request.

This is what we call dynamic components in Next.js, the component is still rendered on the server, but it needs to re-render on each request, unlike Statically Rendered Components.

Dynamic components can't be computed at build time, as the content may change for each request.

Next.js marks the component as dynamic using headers, cookies, and searchParams by itself, as these things can't be computed at build time. Components using and checking for auth are dynamic by default, hence.

Incremental Static Regeneration (ISR) is the same with a little change

ISR is the same as SSG, with just a small enhancement to update the components.

In SSG, the components are rendered at build time on the server and then can only be updated on re-build.

But ISR gives a way to update the component after a certain time window by rebuilding the component again.

Here's how ISR works:

  1. A time window is decided to rebuild the component.

  2. The component is cached during build and served from the CDN.

  3. Requests that occur within the time window get the component cached from the CDN.

  4. Once a request arrives after the time window has expired, the component is marked as stale, the stale component is served to the user, it is rebuilt in the background and given to the CDN to get updated.

  5. Every request after this gets the updated component.

  6. The cycle continues for each time window.

In this way, we can get our components updated after each time window instead of caching them once in the CDN and depending on manual rebuild for updating the components like in SSG.

A request, after the time window expires, triggers the rebuild of the component.

Every visitor gets a ready file, and all rendering happens when nobody is waiting on it. This pattern is called stale-while-revalidate: serve what you have now, refresh it behind the scenes.

In Next.js, turning a page into ISR takes just one line. You export a revalidate value from the page, which is the time window in seconds.

For example, using ISR for a blog listing that changes occasionally:

// src/app/blog/page.tsx

export const revalidate = 60; // rebuild at most once every 60 seconds

export default async function BlogPage() {
    const response = await fetch(`https://mysite.com/posts`);
    const posts = await response.json();

    return (
        <div>
            <h2>Latest Posts</h2>
            <ul>
                {posts.map((post) => (
                    <li key={post.id}>{post.title}</li>
                ))}
            </ul>
        </div>
    );
}

You can also set the time window on an individual fetch instead of the whole page:

const response = await fetch(`https://mysite.com/posts`, {
    next: { revalidate: 60 },
});

Using Next.js 16 with Cache Components?

If your project has cacheComponents enabled, the revalidate export doesn’t apply. Instead, you mark what should be cached with the "use cache" directive and set the time window with cacheLife.

First, to enable it:

// next.config.ts

import type { NextConfig } from "next";

const nextConfig: NextConfig = {
    cacheComponents: true,
};

export default nextConfig;

Then, the same blog page rewritten in a new way:

// src/app/blog/page.tsx

import { cacheLife } from "next/cache";

export default async function BlogPage() {
    "use cache";
    cacheLife("minutes"); // refresh roughly every minute in the background

    const response = await fetch(`https://mysite.com/posts`);
    const posts = await response.json();

    return (
        <div>
            <h2>Latest Posts</h2>
            <ul>
                {posts.map((post) => (
                    <li key={post.id}>{post.title}</li>
                ))}
            </ul>
        </div>
    );
}

So which is best then

Literally, no rendering method is best, which one you should use totally depends on your use case and the type of app you are building.

Want complete client-side control with instant navigation? Go with CSR.

Is the content different for each user or request? Use SSR.

Is the content the same for everyone and rarely changes? Use SSG.

Is it the same for everyone but updates periodically? Use ISR.

No comments yet. Be the first to comment.