CONTACT US
TABLE OF CONTENTS

Next.js Pre-rendering and Data Fetching Methods

Introduction

Just a couple of years ago, all websites were rendered on the server before being served to users. Then, client-side libraries and frameworks like React, Angular, and Vue gained popularity, despite challenges like poor SEO. Server-side rendering (SSR) has since made a comeback and today we’ll look into different Next.js pre-rendering methods.

Next.js is one of the most flexible SSR frameworks for React. It’s trusted by established companies, like Uber or Netflix thanks to its features that make balancing static and dynamic data fetching easier and offer developers even more flexibility.

Static Generation

Static Generation is the default way that Next.js pre-renders pages. The page’s HTML is generated at a build time. It’s only created once and then reused after, giving us the feeling of a fast performance. If the page is static (meaning its content does not change), then it can be cached by a CDN (Content Delivery Network) and served fast. This is perfect for pages where the content doesn’t change much, nor often, for example, landing pages, or login pages.

What about pages that require data fetched from an API for example? Is SSG useless for such pages? Not at all! By exporting a function called getStaticProps we can tell Next.js to fetch the data during the build phase.

Dynamic Paths in SSG

SSG also supports dynamic paths meaning we can generate pages with data on the specific resource.

To do so we have to export another function called getStaticPaths which has to return an array of objects containing route parameters that will identify our resource, for example, a post’s ID. Next.js will then use these parameters to generate pages for each of the resources. It works great for pages with content that rarely changes, like blog posts, recipes, or product listings.

SEO in SSG

Statically generated pages also have the best possible SEO, since they do not require any JavaScript to be run. Search engine bots often have trouble with indexing pages that require JavaScript. It’s the same with Facebook or X crawlers. When you share a post on your fan page, you get that nice preview with the page’s contents, and getting that content is the job of those crawlers. Unfortunately, they cannot run JS at all, which is why SSG is perfect for the job.

What is a Bounce?

A bounce is a situation where a user opens a single page on your site and then exits without going through any other pages. How well your site performs has a huge impact on the bounce rate.

“The probability of bounce increases 32% as page load time goes from 1 second to 3 seconds.”

Google/SOASTA Research, 2017

We want the user to stay and browse through our site and possibly make a purchase of our product. Thanks to static generation we can be sure that page load times are always below 1 second and often even faster

SSG Example

Here you have a simple example of a statically generated page. The todos are fetched in getStaticProps from a JSON file inside the project and mapped to an object with just an ID and a title. The array of those objects is returned in props as todos. Additionally, a generationDate prop is returned, which will tell us when the page was generated. 

const Todos = ({ todos, generationDate }) => {
  return (
    <>
      <h3>Generated on: {generationDate}</h3>

      <ul>
        {todos.map((todo) => (
          <li key={todo.id}>
            <Link href={`todos/${todo.id}`}>{todo.title}</Link>
          </li>
        ))}
      </ul>
    </>
  );
};

export async function getStaticProps() {
  const filePath = path.join(process.cwd(), "data", "todos.json");
  const todosData = fs.readFileSync(filePath, "utf8");

  return {
    props: {
      todos: JSON.parse(todosData).todos.map((todo) => ({
        id: todo.id,
        title: todo.title,
      })),
      generationDate: new Date().toLocaleTimeString(),
    },
  };
}

export default Todos;

Running a yarn build command reveals another great feature of Next.js, the build command prints information about how each page was generated.

Running a yarn build command ion Nextjs

An empty circle means that the page is static (meaning there are no initialProps, no data is needed for generating that page).

A white circle means that the page is either generated during build time or if there is “ISR: x seconds” near the path – the page has ISR enabled.

A lambda symbol means that the page is server side rendered.

localhost todos statistically generated -  Next.js Pre-Rendering Methods

After a successful build, we can run a yarn start and access our application. We can see that refreshing the page won’t change the generation date – we’ve confirmed that the page is in fact created only once during build time.

To display the todo’s details, we need dynamic paths. The implementation of getStaticPaths fetches all of the todos and maps them to an array of params objects.

Each of them contains a key named id with a value of a todo’s id. If we look down at getStaticProps we can see params inside of a context object again with a property id.

The important thing is that the name of the parameter returned from getStaticPaths must match the name of a parameter used in getStaticProps.

const Todo = ({ todo }) => {
  return (
    <div>
      <h3>{todo.title}</h3>
      <p>{todo.content}</p>
    </div>
  );
};

export async function getStaticPaths() {
  const filePath = path.join(process.cwd(), "data", "todos.json");
  const todosData = fs.readFileSync(filePath, "utf8");
  const todos = JSON.parse(todosData).todos;

  return {
    paths: todos.map((todo) => ({
      params: {
        id: todo.id,
      },
    })),
    fallback: false,
  };
}

export async function getStaticProps(context) {
  const todoId = context.params.id;
  const filePath = path.join(process.cwd(), "data", "todos.json");
  const todosData = fs.readFileSync(filePath, "utf8");
  const todos = JSON.parse(todosData).todos;

  return {
    props: {
      todo: todos.find((todo) => todo.id === todoId),
    },
  };
}

export default Todo;

Fallback

A fallback is a property that determines what Next should do when a request to a page that hasn’t been generated comes in. It can be set to three different values – true, false and blocking.

When set to false, any paths not generated at build time (not returned from getStaticPaths) will result in a 404 page. This means that adding new pages will require running the build again. With that in mind, we can easily conclude that this should only be used for sites with a small number of pages that do not change often.

What if our site has a lot of pages? Would that mean having to generate thousands of pages during the build? Wouldn’t that take ages?

Well, yes, this is a situation when the fallback should be set to true. Then, requests won’t result in 404 pages, instead Next will serve a fallback page

A fallback page could be a page with loading skeletons or a piece of nice information to the user that the content he’s requesting will be delivered in a short while. During that time, the requested page will be generated, cached and then served to the user.

This solves the problem of long builds and allows serving an unlimited number of pages. A good idea would be to return only the most visited pages from getStaticPaths and let the rest of them generate at runtime.

However, the above solution has one problem – it requires JavaScript to transition from a fallback page to a regular one.

Blocking in Fallback

As mentioned before, search engine bots, Facebook, and Twitter crawlers either can’t run or aren’t good at running scripts. This is a serious issue for sites that rely on good SEO and social media.

To solve this problem, the third value of fallback has been added – blocking. It works the same way as the previously described value, but the difference is the fallback version is not served. Instead, the page is generated on the server, served to the user, and then cached.

However, the downside is that the user has to wait for the page to be rendered.

Server-Side Rendering

Not every page can be statically generated, for example, if the page contains frequently updated data or is based on the user’s request.

This is where Server-Side Rendering or SSR comes in. Instead of generating the page once at build time, Next.js does that on every request ensuring that the data is always up-to-date. Since the page is rendered on the server just like with SSG, SSR provides the same level of SEO as SSG. This makes SSR a good fit for pages like news feed, marketing pages, etc.

Unfortunately, it has its downsides as well.

Rendering the page on the server means that the user has to wait for the page to be served, depending on the size of the page’s data, the server’s performance, and the current load that could take even a few seconds.

To mitigate that, only metadata could be fetched and rendered on the server side and the rest of the content could be fetched on the client’s side. Of course, this won’t solve all the issues, which is why SSR should be used only when needed.

As you can see, the implementation of getServerSideProps is identical to getStaticProps (Only the implementation is the same, both of these methods differ in the received context object as well as the returned object.)

const TodosSSR = ({ todos, generationDate }) => {
  return (
    <>
      <h3>Generated on: {generationDate}</h3>
      <ul>
        {todos.map((todo) => (
          <li key={todo.id}>
            <Link href={`todos/${todo.id}`}>{todo.title}</Link>
          </li>
        ))}
      </ul>
    </>
  );
};

export async function getServerSideProps() {
  const filePath = path.join(process.cwd(), "data", "todos.json");
  const todosData = fs.readFileSync(filePath, "utf8");

  return {
    props: {
      todos: JSON.parse(todosData).todos.map((todo) => ({
        id: todo.id,
        title: todo.title,
      })),
      generationDate: new Date().toLocaleTimeString(),
    },
  };
}

export default TodosSSR;
localhost3 todos in ssr -  Next.js Pre-Rendering Methods

As you can see, after each refresh the generation date changes, meaning that the page is generated each time we make a request.

Client-Side Rendering

Before explaining what Client Side Rendering is we should note that this isn’t CSR per se.

The classic CSR is when the server returns minimal HTML and a JavaScript bundle that then runs in the client’s browser rendering the content. The downside of that is that when the user starts a session, he has to wait for the bundle to be downloaded and executed during which all he sees is a blank white page.

That’s not very user friendly, and it increases the aforementioned bounce rate.

Well, Next.js has a way to solve that.

What will now be described is just SSG with the data being fetched on the client’s side.

Thanks to Automatic Static Optimization pages by default are server-side generated. So, if no function is exported from a page, it will be generated during the build. We can take advantage of that by adding data fetching on the client’s side. Now, the user will be quickly served a static page with the data being fetched on his side. Instead of a blank screen, the user will see a page with static content and loaders indicating that the rest of the data will soon be available.

Client-side rendering is great for pages where SEO is not crucial, like dashboards, pages requiring authentication, or any other page that will not be indexed by search engines.

The lack of getStaticProps or getServerSideProps indicates that this page is rendered during build time. The data is fetched inside of a useEffect hook.

const TodosCSR = () => {
  const [todos, setTodos] = useState([]);

  useEffect(() => {
    const fetchTodos = async () => {
      const res = await getTodos();
      setTodos(res);
    };

    fetchTodos();
  }, []);

  return (
    <>
      <h3>This header is statically generated</h3>

      <ul>
        {todos.map((todo) => (
          <li key={todo.id}>
            <Link href={`todos-csr/${todo.id}`}>{todo.title}</Link>
          </li>
        ))}
      </ul>
    </>
  );
};

export default TodosCSR;
CSR generated header  Next.js Pre-Rendering Methods

As we can see, clicking the links takes us immediately to the CSR page, and the header is visible immediately, because it was rendered during build time. The todos list, however, shows up after some time as it’s fetched on the client side.

Incremental Static Regeneration

Incremental Static Regeneration or ISR is a hybrid between SSG and SSR and it brings the best of both worlds – The speed of server-side generated pages and up-to-date data of server-side rendering.

How does it work? It works by generating static pages at build time just like SSG. The difference is that each page is revalidated in the background and the old page is swapped for the new one. The interval at which a page is revalidated can be controlled per page by setting a value of the revalidate (in seconds) field of getStaticProps function. 

A common misconception is that it works by revalidating the page every interval. But that would put a lot of load on the API. Imagine having thousands of pages, each of them revalidating itself every minute.

To avoid that the Next.js team decided to implement a stale-while-revalidate caching strategy.

Stale-While-Revalidate Caching

Each time a request for a page is made, the age of the currently cached page is compared against the revalidate value. The cached (stale) page is served to the user and if the comparison results in the age being greater than revalidate, the page is revalidated in the background (hence the name stale-while-revalidate, serve stale data to the user while revalidating).

The result of revalidation replaces the previous value in cache. This means that if the page hasn’t been visited for a week, then no matter the value of revalidate, it won’t be revalidated, and this prevents unnecessary API requests.

We can see that the code is identical to the SSG one. The difference is that we are returning the revalidate prop set to 5.

const TodosISR = ({ todos, generationDate }) => {
  return (
    <>
      <h3>Generated on: {generationDate}</h3>
      <ul>
        {todos.map((todo) => (
          <li key={todo.id}>
            <Link href={`todos-isr/${todo.id}`}>{todo.title}</Link>
          </li>
        ))}
      </ul>
    </>
  );
};

export async function getStaticProps() {
  const filePath = path.join(process.cwd(), "data", "todos.json");
  const todosData = fs.readFileSync(filePath, "utf8");

  return {
    props: {
      todos: JSON.parse(todosData).todos.map((todo) => ({
        id: todo.id,
        title: todo.title,
      })),
      generationDate: new Date().toLocaleTimeString(),
    },
    revalidate: 5,
  };
}

export default TodosISR;
Incrementally generated todo list - Next.js Pre-Rendering Methods

We can see that refreshing the page has no effect on the generation date for the first 5 seconds. Then it changes by 5 seconds – the interval at which the revalidation happens.

Automatic Static Optimization

Next.js determines how each page should be rendered, and it does that by utilizing Automatic Static Optimization. It determines whether the page has exported getServerSideProps or getInitialProps, if not – the page can be pre-rendered (optimized).

This means that Next.js allows us to create hybrid applications that have both server-side generated and rendered pages.

The pre-render method is set per page. It’s up to us to decide how each of them should be pre-rendered.

We can have the landing, login, and register page statically generated, and our marketing page server-side rendered. The product listing page could utilize ISR, while the user’s account settings page could use client-side rendering. All in one app, using just one framework!

Next.js Pre-Rendering Methods – Summary

The Next.js framework provides everything needed to create fast, SEO, and user-friendly applications. Next.js pre-rendering methods aren’t limiting, are easy for developers to use and understand, and have great TypeScript support.

Many established companies, like Netflix or Uber, decided to use Next.js for those very reasons. It could be the solution to boosting the business, marketing, and development you were looking for.

Read More

What is Next JS

Pros and Cons of Next JS

How Can Next.JS Improve UX in E-commerce?

30 Great Examples of Next.js Websites

Next js for E-Learning Platforms

Sanity and Nextjs for CI / CD

Why choose Next js for Order Management Systems

Rafał Dąbrowski

Rafał joined Pagepro in 2021 as a Senior React JS Developer, bringing with him a rapidly developed expertise in web development that began just two years earlier, in 2019. He is renowned for his deep understanding of React JS, a key tool in modern web development. His ability to harness the full potential of React JS has made him a pivotal figure in the development team.

Article link copied

Close button