What is the difference between fallback false vs true vs blocking of getStaticPaths with and without revalidate in Next.js SSR/ISR?

As of Next.js 10, the getStaticPaths function returns an object that must contain the very important fallback key as documented at: https://nextjs.org/docs/basic-features/data-fetching#the-fallback-key-required

While the documentation is precise, it is quite hard to digest for someone that is just beginning with Next.js, could someone try to provide a simpler or more concrete overview of those options?


Solution 1:

How to test

First of all, when testing things out to make sure I had understood them, I was getting really confused because when you run in development mode (next dev) the behavior is quite different than when running in production mode (next build && next start), as it is much more forgiving to help you develop quickly. Notably, in development, getStaticPaths gets called on every render , so everything always gets rendered to their latest version, which is unlike production where more caching might be enabled.

The docs describe the production behavior, so to test things out, you really need to use production mode.

The next issue is that I couldn't easily find an example where you can create and update pages from inside the example itself to easily view their behavior. I finally ended up doing that at: https://github.com/cirosantilli/node-express-sequelize-nextjs-realworld-example-app while porting the awesome Realworld example project, which produces a simple multiuser blog website (mini Medium clone).

With those tools in hand, I was able to confirm what the docs say. This answer was tested at this commit which has Next.js 10.2.2.

fallback: false

This one is simple: only pages that are generated during next build (i.e. returned from the paths property of getStaticPaths) will be visible.

E.g., if a user creates a new blog page at /post/[post-id], it will not be immediately visible afterwards, and visiting that URL will lead to a 404.

That new post will only become visible if you re-run next build, and getStaticPaths returns that page under paths, which is the case for the typical use case where getStaticPaths returns all the possible [post-id].

fallback: true

With this option, Next checks if the page has been pre-rendered to HTML under .next/server/pages.

If it has not:

  1. Next first quickly returns a dummy pre-render with empty data that had been created at build time.

    In this, you are expected to tell the user that the page is loading.

    You must handle that case, or else it could lead to exceptions being thrown due to missing properties.

    The way to handle this is described in the docs by checking router.isFallback:

    import { useRouter } from 'next/router'
    
    function Post({ post }) {
        const router = useRouter()
    
        // If the page is not yet generated, this will be displayed
        // initially until getStaticProps() finishes running
        if (router.isFallback) {
            return <div>Loading...</div>
        }
    
        // Render post...
        if (router.isFallback) {
            return <div>Loading...</div>
        }
    
        return <div>post.body</div>
    
    }
    

    So in this example, if we hadn't done the router.isFallback check, post would be {}, and doing post.body would throw an exception

  2. After the actual page finishes rendering for the first time with data (the data is fetched with getStaticProps at runtime), the user's browser gets automatically updated to see it, and it stores the resulting HTML under .next/server/pages

If the page is present under .next/server/pages however, either because:

  • it was rendered by next build
  • it was rendered for the first time at runtime

Next.js just returns it, without rendering again.

Therefore, If you edit the post, it will not re-render the page cache. The outdated page will be returned at all times, because it is already present under .next/server/pages, so next does not re-render it.

You will have to re-run next build to see updated versions of the pages.

Therefore, this is not what you generally want for the multi-user blog described above. This approach is generally only suitable for websites that don't have user-generated content, e.g. an e-commerce website where you control all the content.

fallback: true: what about pages that don't exist?

If the user accesses a page that does not exist like /post/i-dont-exist, Next.js will try to render it just like any other page, because it checks that it is not in .next/server/pages thinks that it just hasn't been rendered before.

This is unlike fallback: false, where Next.js never generates new pages at runtime, and just returns a 404 direction.

In this case, your code will notice that the page does not exist when getStaticProps queries the database, and then you tell Next.js that this is a 404 with notFound: true as mentioned at: How to return a 404 Not Found page and HTTP status when an invalid parameter of a dynamic route is passed in Next.js? so Next.js renders a 404 page and caches nothing.

fallback: 'blocking'

This is quite similar to fallback: true, except that it does not return the dummy loading page when a page that hasn't been cached is hit for the first time

Instead, it just makes the browser hang, until the page is rendered for the first time.

Future requests to that page are quickly served from the cache however, just like fallback: true.

https://dev.to/tomdohnal/blocking-fallback-for-getstaticpaths-new-next-js-10-feature-1727 mentions the rationale for this, it appears to break certain rather specific features, and is generally not what you want unless you need one of those specific features.

Note that Next.js documentation explicitly states that in fallback: true, it detects crawlers (TODO how exactly? User agent or something else? Which user agents), and does not return the loading page to crawlers, which would defeat the purpose of SSR. https://nextjs.org/docs/basic-features/data-fetching#the-fallback-key-required mentions:

Note: this "fallback" version will not be served for crawlers like Google and instead will render the path in blocking mode.

so there doesn't seem to be a huge advantage for SEO purposes in using 'blocking' over true.

However, if your user is a security freak and disables JavaScript, they will only see the loading page. And are you sure the Wayback machine won't show the loading page? What about wget? Since I like such use cases, I'm tempted to just use fallback: 'blocking' everywhere.

revalidate: Incremental Static Regeneration (ISR)

When revalidate is given, new requests to a page that is in the .next/server/pages cache also make the cache be regenerated. This is called "Incremental Static Regeneration".

revalidate: n means that our server will do at most 1 re-render every n seconds. If a second request comes in before the n seconds, the previously rendered page is returned and a new re-render is not triggered. So large n means users see more outdated pages, but less server workload.

A large re validate could therefore help the server handle large traffic peaks by caching the reply.

This is what we have to use if we want website users to both publish and update their own posts:

  • either fallback: true or fallback: 'blocking'
  • together with revalidate: <integer>

revalidate does not make much sense with fallback: false.

When revalidate: <number> is given, behavior is as follows:

  • if the page is present under .next/server/pages, return this prerendered immediately, possibly rendered with outdated data.

    At the same, also kickstart a page rebuild with the newest data.

    When the rebuild is finished, the target page won't be automatically updated to the latest version. The user would have to refresh the page to see the updated version.

  • otherwise, if the page is not cached, do the same that true or 'blocking' would do, by either returning a dummy wait page, or blocking until it gets done, and create the cached page

After a page is built by either of the above cases (first time or not), if it gets accessed again in the next number seconds, do not trigger rebuilds. This way, if a very large number of users is visiting the website, most of the requests won't require expensive server render work: we will do at most one re-render every number seconds.

SSR for a single request (i.e. ignore revalidate) so that users can see the results of their blog page edits

This can be achieved with "preview mode" which is documented at: https://nextjs.org/docs/advanced-features/preview-mode It was added in Next.js 12. Preview mode checks if come cookies are set, and if so makes getStaticProps rerun regardless of revalidate, just like getServerSideProps. However, it still does not solve my use case super nicely, because preview mode does not invalidate/update the cache, which is a widely requested thing, related:

  • Next.js ISR ( Incremental Static Regeneration ), how to rebuild or update a specific page manually or dynamically before the interval/ISR time start?
  • How to clear/delete cache in NextJs?

so it could still happen that the user visits the page without cache and sees the outdated page. I could work around this by removing the cookies and making a an extra GET request, but this produces an useless get request and adds more complexity.

If for example:

  • the blog author clicks submit after updating an existing post
  • and they got redirected to the post view page as is usual behaviour, to see if everything looks OK

they first see the outdated version of the post. Then redirected this visit would trigger a rebuilt with the new data they've provided in the edit page, and only after that finishes and the user refreshes they would see the updated page.

So this behavior is also not ideal UI behavior for the editor, as the user would be left thinking:

What just happened, was my edit not registered?

for a few seconds.

Preview mode solves precisely this problem.

I learned about this after opening an issue about it at: https://github.com/vercel/next.js/discussions/25677 thanks to @sergioengineer for pointing it out.

Related threads:

  • https://github.com/vercel/next.js/discussions/11698#discussioncomment-351289
  • https://github.com/vercel/next.js/discussions/11552

SSR vs ISR: per user-login-based information

ISR is an optimization over SSR. However, like every optimization, it can increase the complexity of the system.

For example, suppose that users can "favorite" blog posts.

If we use ISR, it only makes much sense to pre-render a logged off page, because it only makes sense to pre-render the stuff that is common for multiple users.

Therefore, if we want to show to the user the information:

Have I starred this page yet or not?

then we have to do a second API request and then update the page state with it.

While it may sound simple, this adds considerable extra complexity to the code in my experience.

With SSR however, we could simply check the login cookies sent by the user as usual, and fully render the page perfectly customized to the current user on the server, so that no further API requests will be needed. Much simpler.

So you should really only do it if you benchmark it and it is worth it.

The ISR dream: infinite revalidate + explicit invalidation + CDN hooks

As of Next.js 12, ISR is wonky for such a CRUD website, what I would really want is for things to work as follows:

  • when the user creates a blog post, we use a post creation hook to upload the result to a CDN of choice
  • when a user views a blog post, it goes to the CDN directly and does not touch the server. Only if the user wants to fetch user-specific data such as "have I starred this page" does it make a small API request to the server
  • when a user updates a blog post, it just updates the result on the CDN of choice

This approach would really lead to ultra-fast page loads and minimal server workload.

I think Vercel, the maintainers of Next.js, do have such a system running on their product, but I don't see how to nicely use an arbitrary CDN of choice, because I don't see such hooks. I hope I'm wrong :-)

But just the explicit invalidation + infinite revalidate would already be a great thing to have even without the hook system.