Avatar

Hello, I'm Julia.

NextJS Page Rendering Options

#nextjs #react

8 min read

One amazing feature about the NextJS framework is all of the various options one has around how to render a page, which can be determined on a page-by-page basis. The downside to this is that it can be quite overwhelming to NextJS newcomers. I therefore thought I’d write this blog post to try to summarise the various rendering options and why one might choose one over another.

Note that the Vercel team are constantly improving the framework so be sure to check the docs for the latest updates!

Pre-rendering - what is it?

  • Plain React apps have no pre-rendering. This means nothing is seen on initial load as nothing is immediately rendered. We need to wait for the JS to load, before hydration can happen (when React components are initialised and the app becomes interactive).
  • NextJS uses pre-rendering, meaning the server serves initial HTML which can be displayed on initial load (faster time to first byte i.e. faster performance). The JS is then loaded and hydration occurs.

Static generation

  • This is when HTML is generated at build-time and reused for every request.
  • This is done using the next build command, which happens prior to any request being made i.e. at build time.
  • In practice, if we deployed on Vercel, we would have deployed this file to Vercel’s servers, which caches it so it can be reused for each request.
  • Note: You might see references to SSG. This means Static Site Generation.

Types of static generation

  • There are 2 types of static generation: (i) without data and; (ii) with data.
    • If there is no data required, you can just code your components and run next build.
    • If data is required, use the getStaticProps method to fetch the data you need at build time (e.g. from an API or internal data set). This data can then be passed into your component as props.
      function Store({ products }) {
        return (
          <ul>
            {products.map((product) => (
              <li>product.name</li>
            ))}
          </ul>
        )
      }
      
      export async function getStaticProps(context) {
        const res = await fetch(`https://${yourEndpoint}`)
        const data = await res.json()
      
        return {
          props: {
            products: data.products,
          },
        }
      }
      
      export default Store

Pre-rendering page paths during static generation

  • You can pre-render pages with paths that depend on external data. e.g. if you have a blog with lots of different posts. This is done with getStaticPaths.
    // pages/posts/[id].js
    export async function getStaticPaths() {
      const res = await fetch(`https://${yourEndpoint}`)
      const posts = await res.json()
    
      const paths = posts.map((post) => {
        params: {
          id: post
        } // Note: id name here must match the [id] param in page name
      })
    
      return {
        paths,
        fallback: false, // Other options: true, 'blocking'
      }
    }
  • If fallback: false is set, a 404 page is shown if a user goes to a page whose path doesn’t exit.
    • fallback: true is useful in cases where you only want to generate a subset of pages during build time (e.g. if you have a lot of pages and don’t want to bog down your build time). If a user requests a page that wasn’t statically generated, they’ll first be shown a fallback version of the page (e.g. with loading spinners and a skeleton layout), whilst a request to statically generate the page (including getStaticProps) is run in the background. The page will automatically be rendered with the content once the request is complete.
    • fallback: blocking is somewhat similar to true except that nothing is shown to the user while the HTML generation is happening (similar to SSR which I’ll explain below). Once generated, this page is cached for future requests so that this only happens once per path.
  • More information in the official docs here.

Incremental Static Regeneration (ISR)

  • Similar to static generation with data above, but instead of the data fetching happening at build time, it happens at request time instead.
    function Store({ products }) {
      return (
        <ul>
          {products.map((product) => (
            <li>product.name</li>
          ))}
        </ul>
      )
    }
    
    export async function getStaticProps(context) {
      const res = await fetch(`https://${yourEndpoint}`)
      const data = await res.json()
    
      return {
        props: {
          products: data.products,
        },
        revalidate: 60, // <-- this is the key addition! Note: in seconds.
      }
    }
    
    export default Store
  • Setting revalidate: 60 means that we’re saying, “After 60 seconds, this content is stale”. Note this sets the “Cache-Control” HTTP header, stale-while-revalidate response.
  • If a user come to the site within the first 60 seconds, they will see the static HTML generated at build time.
  • After 60 seconds, any user that comes to the page will see stale content. This kicks off a request in the background however, where we call getStaticProps. Assuming a successful response, this invalidates the cache and updates it with the new content. The next user that comes to the site will now see this updated content.
  • ISR allows us to somewhat combine static generation at build time and static generation at request time, which means not having to constantly rebuild the whole application whenever there’s a page update.

Server-side rendering (SSR)

  • HTML is generated on every request. Used when you have very dynamic / personalised content, that needs to be fresh at every request.
  • The request for data is done in a blocking manner so the user doesn’t see any page content until the request is complete. This is done via the getServerSideProps function (fairly similar in implementation to getStaticProps).
    function Store({ products }) {
      return (
        <ul>
          {products.map((product) => (
            <li>product.name</li>
          ))}
        </ul>
      )
    }
    
    export async function getServerSideProps(context) {
      const res = await fetch(`https://${yourEndpoint}`)
      const data = await res.json()
    
      return {
        props: {
          products: data.products,
        },
      }
    }
    
    export default Store
  • Deciding when to use SSR vs. ISR can be tricky.
    • Note that ISR with fallback: ‘blocking’ is effectively like SSR so think about whether you really want this.
    • SSR is generally better if you have edge cases that you need to deal with server-side, as it provides more flexibility than ISR e.g. having to deal with data from multiple domains, depending on request headers. This is because SSR gives you access to the request object, whereas ISR does not.
    • Another use case is when you need to serve personalised content but don’t want to use CSR (see below).

Client-side rendering (CSR)

  • Used when we want static generation without data, but want to fetch data on the client-side.
  • A practical example might be:
    • During build time, we generate a loading skeleton layout.
    • This means that when a user lands on the page, this is what they’ll immediately see, while waiting for the JavaScript to load.
    • Once the JS finishes loading client-side, we can then fetch external data and populate the page with the response data (assuming it’s successful).
  • Vercel has a custom hook to make this easy called useSWR.
    import useSWR from 'swr'
    
    function Dashboard() {
      const { data, error } = useSWR('/api/user/dashboard', fetcher)
    
      if (error) return <div>Something went wrong</div>
      if (!data) return <div>Loading</div>
      return <div>{data.name}'s Dashboard</div>
    }
    • <div>Loading</div> This is first shown before the JS completes its loading.
    • Once the JS is loaded, it runs the useSWR('/api/user/dashboard', fetcher) command.
    • If this is successful, we then show <div>{data.name}'s Dashboard</div>.
  • This is great for improving performance by decreasing time to first byte (TTFB). It’s especially useful if you need to make a lot of requests to get the content you need, where SSR could be detrimental due to requests being blocking until complete.

Conclusion

  • NextJS is a hybrid framework that allows us to choose which rendering strategy we want on a per-page basis.
  • Examples of when you might want to use certain strategies:
    • A standard blog that pulls posts from a headless CMS - static generation with data.
    • A blog that has millions of posts, pulled from a headless CMS - static generation with data, but with fallback: true assuming you only generate a subset of pages during build time.
    • A personalised page that only has one section requiring a fast, but complex API call - server side rendering.
    • An authenticated user’s data dashboard with lots of different charts - client side rendering.
    • Landing page which is really important for SEO - ensure the fastest time to first byte by not using SSR. Use static generation without data is there is no dynamic data.
    • Authenticated data for internal teams - server side rendering (because you don’t particularly care about TTFB anyway).

Web Mentions

Tweet about this post and have it show up here!

© 2016-2022 Julia Tan · Powered by Next JS.