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:
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...
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
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.
Explicit invalidation with: res.unstable_revalidate
This is currently beta, but it seems that at last they are introducing a way to explicitly invalidate pages as currently mentioned at: https://vercel.com/docs/concepts/next.js/incremental-static-regeneration
This way, instead of ugly revalidate
timeouts, we will be able to just rebuild pages only when needed if we are able to detect the page becoming outdated on the server. Then we can just sever directly every time.
It will presumably be renamed to res.revalidate
once it becomes stable.
This awesome development was brought to my attention by Sebastian in the comments.
SSR for a single request (i.e. ignore revalidate
) so that users can see the results of their blog page edits
Edit: this use case might best resolved with the upcoming res.unstable_revalidate
/res.revalidate
.
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 behavior, 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.
This can be solved 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, even preview mode does not solve this use case super nicely, because it does not invalidate/update the cache, which is a widely requested thing, related:
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.
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:
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.
Here's an example of checking login cookies: https://github.com/cirosantilli/node-express-sequelize-nextjs-realworld-example-app/blob/8dff36e4bcf659fd048e13f246d50c776fff0028/back/IndexPage.ts#L23 That sample setup uses the exact same SWR tokens that are being used to make JavaScript API requests but also via cookies. We don't have to worry about XSS in that demo because we only use login on GET requests. All modifying requests like POST are done from JavaScript exclusively, and don't authenticate from cookies.
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 Nirvana.
I think Vercel, the company behind Next.js, might such a CDN 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 CDN hook system. Edit: this might be coming with res.unstable_revalidate
, see section above.