TABLE OF CONTENTS

Next.js Pre-rendering and Data Fetching Methods

Rafał Dąbrowski
By Rafał Dąbrowski
Nextjs prerender methods

Introduction

Just a couple of years ago all websites were rendered on the server and then served to the user.

Then, an influx of client-side libraries and frameworks started (React, Angular, Vue, and more) and their benefits overshadowed their drawbacks, like poor SEO.

But now in 2021 server-side rendering has come back to life and in this article, I will focus on the most versatile of SSR React’s frameworks – Next.js.

Static Generation

Static Generation is the default way that Next.js pre-renders pages. Static Websites are also becoming more and more popular under the name of Jamstack development. But maybe you want to know What is Jamstack anyway?

Going back to SSG, with static generation, the page’s HTML is generated at a build time. It means that it is only created once and then reused after, giving us the feeling of blazing performance.

Why is that? If the page is static (meaning its content does not change), then it can be cached by a CDN (Content Delivery Network) and served really fast. This is perfect for pages where the content doesn’t change much, nor often, for example landing pages, login pages, etc.

What about pages that require data that has to be 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!

SSG also supports dynamic paths meaning we can generate pages with data of 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 posts’ ID. Next.js will then use these parameters to generate pages for each of the resources (we don’t have to return all of them, Next.js can generate them during runtime. More on that later in the article). This is perfect for pages with content that changes rarely, for example, blog posts, recipes, or product listings or details.

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 Twitter 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.

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

~Google/SOASTA Research, 2017.

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.

Obviously, we want the opposite, 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!

Below is 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

After a successful build we can run 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 the 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 amount 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 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 the regular one.

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.

More on the topic of server side rendering below.

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

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 actually just SSG with the data being fetched on the client’s side.

Thanks to Automatic Static Optimization (more on that later in the article) 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 doesn’t matter. It can be used for 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 (More on Automatic Static Optimization further in the article). 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

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. That’s a lot of requests!

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

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

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 has to determine 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, meaning that 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, our marketing page server-side rendered, our products listing page could utilize ISR, while the user’s account settings page could use client-side renderingall in one app, using just one framework!

Summary

Next.js is a tool that provides everything that is needed to create fast, SEO, and user-friendly applications.

It’s worth noting that Next.js is among the fastest-growing frameworks in the world. With no doubt, its features will be extended even more in the near future. It doesn’t limit us to one method of pre-rendering. Like other frameworks (Gatsby for example), it’s easy for developers to use and understand, has great TypeScript support, and is the future of web development.

This is why it was chosen by many of the biggest web companies like Netflix or Uber and which is why it should be chosen by you to boost your business, marketing, and development.