One of the main Next.js features is page pre-rendering. What does this mean?

Next.js generates HTML for each page in advance, instead of having it all done by client-side JavaScript. Pre-rendering the pages can result in better performance and SEO.

Each generated HTML is associated with minimal JavaScript code necessary for that page. When the browser loads a page, its JavaScript code runs and makes the page fully interactive.

Two types of pre-rendering

When building websites or web applications, you generally have to choose between 2 strategies:

  • Static generation (SSG) or

  • Server-side rendering (SSR).

The main difference between SSG and SSR is that with Static generation, the HTML is generated at build time and will be reused on each request, while on the other hand, with server-side rendering, the HTML is generated on each request.

And the great thing is: Next.js is the first hybrid framework that allows us to choose the technique that fits our use case best on a per-page basis.

Next.js developers recommend using Static generation over Server-side Rendering for performance reasons. CDN can cache statically generated pages with no extra configuration to boost performance. However, in some cases, Server-side Rendering might be the only option.Static generation

As previously mentioned, if a page uses Static Generation, the page HTML is generated at build time. That means in production, and the page HTML is generated when you run next build. This HTML will then be reused on each request. A CDN can also cache it.

In Next.js, we can statically generate pages with or without data.

A static generation without data

This is the most simple case where the page does not need to fetch any external data to be pre-rendered, and by default, Next.js pre-renders pages using Static Generation without fetching data.

Here is a basic example:

1function About() {
2  return <div>About</div>
3}
4
5export default About

Static generation with data

Before we dive deeper into these examples and variations, I would like to point out some things as a small introduction.

Since version 9.3, Next.js added a new life-cycle method getStaticProps and getStaticPaths for improved SSG support, allowing us to fetch data and create static pages at build time. With the new data fetching method getServerSideProps used for SSR, Next.js allows us to separate code intended for SSG or SSR. This is, in many cases, much better than the classic hybrid approach of getInitialProps (which was introduced in the 9.0 version with the Automatic static optimization concept ).

Some pages require fetching external data for pre-rendering. Considering this, there are two scenarios, and one or both might apply. In each case, you can use mentioned Next.js life-cycle methods:

  • getStaticProps - if our page content depends on external data 

  • getStaticPaths - if our page paths depend on external data (usually in addition to getStaticProps)

If you export an async function called getStaticProps from a page, Next.js will pre-render this page at build time using the props returned by getStaticProps.

1export async function getStaticProps() {
2  return {
3    props: {}, // will be passed to the page component as props
4  }
5}

getStaticProps should return an object with:

Page content depends on external data

We will cover an example that uses getStaticProps to fetch a list of blog posts from a CMS.

1// TODO: Need to fetch `posts` (by calling some API endpoint)
2//       before this page can be pre-rendered.
3function Blog({ posts }) {
4  return (
5    <ul>
6      {posts.map((post) => (
7        <li>{post.title}</li>
8      ))}
9    </ul>
10  )
11}
12
13export default Blog

To fetch this data on pre-render, we will export an async function called getStaticProps from the same file. This way, we can pass fetched data to the page's props on pre-render.

1function Blog({ posts }) {
2  // Render posts...
3}
4
5// This function gets called at build time
6export async function getStaticProps() {
7  // Call an external API endpoint to get posts
8  const res = await fetch('https://.../posts')
9  const posts = await res.json()
10
11  // By returning { props: posts }, the Blog component
12  // will receive `posts` as a prop at build time
13  return {
14    props: {
15      posts,
16    },
17  }
18}
19
20export default Blog

Also, besides this example, a good question that sums up this excellent method is:

When should I use getStaticProps?

  • If the data required to render the page is available at build time ahead of a user’s request.

  • If the data comes from a headless CMS.

  • If the data can be publicly cached (not user-specific).

  • If the page must be pre-rendered (for SEO) and be very fast — getStaticProps generates HTML and JSON files, both of which can be cached by a CDN for performance.

Page paths depend on external data

In Next.js, we can create pages with dynamic routes.

If a page has dynamic routes and uses getStaticProps it needs to define a list of paths that have to be rendered to HTML at build time.

For example, we can create a file called pages/posts/[id].js to show a single blog post based on id. This will allow us to show a blog post with id: 1 when we access posts/1.

Later, we might add the second post with id: 2. Then we’d want to pre-render posts/2 as well.

Our page paths that are pre-rendered depend on external data. To handle this, we can export an async function called getStaticPaths from a dynamic page (pages/posts/[id].js in this case). This function gets called at build time and lets us specify which paths we want to pre-render.

1// This function gets called at build time
2export async function getStaticPaths() {
3  // Call an external API endpoint to get posts
4  const res = await fetch('https://.../posts')
5  const posts = await res.json()
6
7  // Get the paths we want to pre-render based on posts
8  const paths = posts.map((post) => `/posts/${post.id}`)
9
10  // We'll pre-render only these paths at build time.
11  // { fallback: false } means other routes should 404.
12  return { paths, fallback: false }
13}

In the example above, we need to mention that paths and fallback keys are required.

The paths key determines which paths will be pre-rendered. For example, our page uses dynamic routes named pages/posts/[id].js. If we export getStaticPaths from this page and return the following for paths:

1return {
2  paths: [
3    { params: { id: '1' } },
4    { params: { id: '2' } }
5  ],
6  fallback: ...
7}

Then Next.js will statically generate posts/1 and posts/2 at build time using the page component in pages/posts/[id].js.

The object returned by getStaticPaths must contain a boolean fallback key.

If fallback is false, then any paths not returned by getStaticPaths will result in a 404 page. We can do this if we have a small number of paths to pre-render - so they are all statically generated during build time. It’s also useful when the new pages are not added often. If we add more items to the data source and need to render the new pages, we’d need to run the build again.

We will show a full example that pre-renders one blog post per page called pages/posts/[id].js. The list of blog posts will be fetched from a CMS and returned by getStaticPaths. Then, for each page, it fetches the post data from a CMS using getStaticProps:

1// pages/posts/[id].js
2
3function Post({ post }) {
4  // Render post...
5}
6
7// This function gets called at build time
8export async function getStaticPaths() {
9  // Call an external API endpoint to get posts
10  const res = await fetch('https://.../posts')
11  const posts = await res.json()
12
13  // Get the paths we want to pre-render based on posts
14  const paths = posts.map((post) => ({
15    params: { id: post.id },
16  }))
17
18  // We'll pre-render only these paths at build time.
19  // { fallback: false } means other routes should 404.
20  return { paths, fallback: false }
21}
22
23// This also gets called at build time
24export async function getStaticProps({ params }) {
25  // params contains the post `id`.
26  // If the route is like /posts/1, then params.id is 1
27  const res = await fetch(`https://.../posts/${params.id}`)
28  const post = await res.json()
29
30  // Pass post data to the page via props
31  return { props: { post } }
32}
33
34export default Post

We use fallback: true case when our app has many static pages that depend on data (for example: an e-commerce website with lots of products). Find more information about fallback:true

Conclusion

Static Generation (with and without data) should be used whenever possible because the page can be built once and served by CDN, making it much faster than having a server render the page on every request and when the page can be pre-rendered ahead of a user's request.

Static Generation can be used for many types of pages, such as:

  • Marketing pages

  • Blog posts

  • E-commerce product listings

  • Help and documentation

Both SSG methods, getStaticProps, and getStaticPaths are run at a build time and can only be exported from a page. Also, both methods must be export-ed as async functions and will not work if added as a property of the page component.

I would also recommend watching this nice tutorial covering most of the mentioned things and some other features such as using SSG with fs (file-system).

Happy coding!