Next.js can render your React app in a few different ways, and the most surprising thing is that you can mix and match them on a per-page basis.

Let’s see this in action. Imagine a simple Next.js app with two pages:

pages/index.js (Server-Side Rendered - SSR)

function HomePage({ data }) {
  return (
    <div>
      <h1>Welcome!</h1>
      <p>Server fetched data: {data.message}</p>
    </div>
  );
}

export async function getServerSideProps(context) {
  const res = await fetch('https://api.example.com/greeting');
  const data = await res.json();

  return {
    props: {
      data,
    },
  };
}

export default HomePage;

pages/about.js (Static Site Generation - SSG)

function AboutPage({ data }) {
  return (
    <div>
      <h1>About Us</h1>
      <p>Static data: {data.version}</p>
    </div>
  );
}

export async function getStaticProps(context) {
  const res = await fetch('https://api.example.com/version');
  const data = await res.json();

  return {
    props: {
      data,
    },
    revalidate: 60, // Re-generate at most once every 60 seconds
  };
}

export default AboutPage;

When a user visits /, the Next.js server will execute getServerSideProps. It fetches https://api.example.com/greeting, gets {"message": "Hello from the server!"}, and then renders HomePage with this data. The HTML sent to the browser is already complete, including "Server fetched data: Hello from the server!".

When a user visits /about, Next.js initially runs getStaticProps at build time (or on the first request if revalidate is used). It fetches https://api.example.com/version, gets {"version": "1.2.3"}, and renders AboutPage. This static HTML is then cached. If revalidate: 60 is set, Next.js will regenerate this page in the background after 60 seconds if another request comes in.

Client-Side Rendering (CSR) is what happens in a standard React app. You might use it for dynamic dashboards or user-specific content after the initial page load. In Next.js, you’d typically fetch data within a useEffect hook in your component:

import { useState, useEffect } from 'react';

function DashboardPage() {
  const [userData, setUserData] = useState(null);

  useEffect(() => {
    fetch('/api/user-data') // Assuming this is a Next.js API route
      .then((res) => res.json())
      .then((data) => setUserData(data));
  }, []);

  if (!userData) {
    return <div>Loading...</div>;
  }

  return <div>Welcome, {userData.name}!</div>;
}

export default DashboardPage;

Here, the initial HTML for DashboardPage might just be "Loading…". The actual user data is fetched by the browser after the page has loaded and rendered.

The real power comes with Incremental Static Regeneration (ISR), which is essentially SSG with a refresh button. As seen in the /about example, revalidate: 60 means the static page is rebuilt periodically in the background without blocking the user. This is ideal for content that changes but doesn’t need to be real-time, like blog posts or product listings.

To recap, Next.js provides these modes:

  • CSR (Client-Side Rendering): Data fetched in the browser after initial load, typically via useEffect. Good for highly dynamic, user-specific content.
  • SSR (Server-Side Rendering): Data fetched on the server for every request using getServerSideProps. Ensures fresh data on each visit but can be slower and more resource-intensive.
  • SSG (Static Site Generation): Data fetched at build time (or on first request) using getStaticProps. Fastest possible load times as HTML is pre-rendered, but data is static until a new build.
  • ISR (Incremental Static Regeneration): SSG with a twist. getStaticProps with a revalidate option allows static pages to be rebuilt in the background after a specified interval, balancing speed with up-to-date content.

The next build command is where SSG and ISR magic happens. For SSG pages without revalidate, the HTML is generated once and served from the CDN. For ISR pages, Next.js generates the initial HTML, and then on subsequent requests that fall outside the revalidate window, it serves the stale content while generating a fresh version in the background. This background generation is a "stale-while-revalidate" strategy.

The most common confusion arises from getStaticProps vs. getServerSideProps. If your page data is the same for every user and doesn’t change frequently, getStaticProps (with or without ISR) is usually the way to go for performance. If the data is unique to the user or changes very rapidly, getServerSideProps is more appropriate. CSR is a fallback for components that don’t fit neatly into the other categories or for data that is only relevant after initial hydration.

The next major concept you’ll encounter is dynamic routing with these rendering modes.

Want structured learning?

Take the full Nextjs course →