Next.js is an open-source React-based framework created by Vercel. One of the main features offered by Next.js is static site generation. This significantly reduces the render time of a site’s static pages by converting them into HTML during the build process, which are then stored in the server’s cache. This cached HTML is then reused for every request, resulting in faster delivery to users. Static site generation can be done with or without external data being used to generate the page.
Challenges in Static Rendering
While static site generation does come with a significant increase in performance and efficiency by making pages easily cacheable in the browser via content delivery networks (CDNs), it does normally come with the drawback that when webpages rely on dynamic content, like a content management system (CMS), the displayed page may become outdated.
Updating content on a CMS will have no effect on a static page that has already been built. The page will still display the data that it used to create the page at build time, unaware that the external data has changed. Normally, the only way to update the static page is to rebuild the entire website. Doing this for a single page update is an annoying inconvenience for smaller sites and a major problem for large ones with hundreds or thousands of pages that may need regular updates.
Incremental Static Regeneration to the Rescue!
Incremental Static Regeneration (ISR) is the Next.js solution to this problem. With ISR, we can retain the speed benefits of static site generation, keep content up-to-date, and scale to massive numbers of pages. This is done by rebuilding individual static pages as required after the site has already been deployed. ISR offers advantages over the traditional use of Cache-Control headers in that it allows a single cache to be used globally resulting in a higher HIT rate as well as allowing smaller subsets of data to be revalidated resulting in significantly reduced latency associated with fetching and rendering updated content.
There are two ways that we can implement this.
1. Time-based Revalidation
This method involves essentially scheduling updates for your data. By passing an option into your fetch request for the external data necessary for the page, you can determine the lifespan of your cached data. Time-based revalidation is most useful for pages that are updated less frequently, and the need for real-time updates is less critical. We can set a revalidation timer of 3600 seconds on our request using this syntax.
fetch('<https://...>', { next: { revalidate: 3600 } })
When a user accesses the page between the build time and the timer running out, they will be served data from the cache as normal. Once a user loads the page for the first time after this interval has run out, Next.js will serve the data from the cache for that request, retaining the performance benefit, while also fetching fresh data in the background for any subsequent requests. This means that the page is periodically checked to make sure its content is up-to-date.
2. On-Demand Revalidation
On-demand updates can be approached in two ways: by path (invalidating the cache associated with a specific url) or by tag (invalidating one or more caches that we have marked as belonging to a particular collection).
a. Path-Based Revalidation:
This method involves invalidating the data cache linked to a specific URL by calling the revalidate
function on it. The syntax is very simple:
res.revalidate(urlPath)
This function is utilised by calling it upon a specific event occurring. For example, you could set up a webhook in your CMS that is invoked whenever an update is published to any of the content. Once the webhook is called, a listener on your deployed site will check the payload to determine what content was updated and then call res.revalidate()
on the appropriate URL to generate an updated version of that page with new content.
b. Tag-Based Revalidation:
This can be used to easily invalidate a number of different requests at the same time. We can do this by marking our fetch requests as belonging to specific collections.
fetch('<https://...>', { next: { tags: ['collection'] } })
Then, similarly to path-based revalidation, we can call revalidateTag()
on the collection we want regenerated after a specific event has occurred:
revalidateTag('collection')
As an example of why this is useful, let’s say we have a site that displays a number of events people can go to. Events are listed on an event index page, and on each event page we also have a section with links to other similar events.
If we update an event's content, we need to regenerate not only the specific event’s own page but also the event index page and all other pages that may link to the updated event.
To be able to handle this behaviour, we would tag the fetch requests for our event pages with 'event':
Event page fetch request:
fetch('<https://...>', { next: { tags: ['event'] } })
When an event is updated, invalidating the 'event' tag prompts Next.js to rebuild not only the specific event page with new content but also all other events as they may link to the updated event in their similar events section.
We should tag the event index page with ‘event' as well, as we want the index page to be updated along with the events to display the correct information about the newly updated event. On top of this, we also want to have the ability to update the event index page by itself if we are making a change that is specific to it alone, such as updating the index’s page heading. Because of this, the event index page should also have its own unique tag that can be invoked, and so we will pass in both ‘event’ and 'event-index' into our fetch requests for that page.
Event index page fetch request:
fetch('<https://...>', { next: { tags: ['event', 'event-index'] } })
When we then publish a change to the content of an event, our webhook will be invoked. Our handler determines from the payload that it is an event that has been updated and passes the 'event' tag into the revalidate tag function. All our events, along with the event index page, will then regenerate to display the updated content on our already-built site.
This makes it really simple to invalidate a large number of linked pages without finding all the individual paths we need to invalidate and calling invalidatePath()
on all of them.
Error Handling
If an error is thrown while attempting to revalidate data using any of these methods, the last valid data will continue to be used from the cache. On the next subsequent request, Next.js will try to revalidate the data again. This mitigates the risk of erroneous data causing a page to go down unexpectedly.
In Conclusion
Incremental Static Regeneration can be easily added to your Next.js project to make your pages have both the performance of a static page as well as the freshness of a dynamic one. Users will appreciate quick-loading pages that are always up-to-date!