Nuxt ISR and advanced caching on Netlify
Nuxt 3 is the latest version of the powerful Vue based framework, offering streamlined Server Side Rendering (SSR) and modern features like Static Site Generation (SSG) and on-demand rendering with Incremental Static Regeneration (ISR). ISR is particularly useful for sites that need to pre-render pages for fast access but also require occasional data updates without a full site rebuild. With ISR, you can get the best of both worlds: speed of static generation and freshness of server side rendering.
What is ISR?
Incremental Static Regeneration or ISR is a way to render pages that lets you cache a page when first requested and control when that page is regenerated - either after a set timeframe or with on-demand invalidation.
Why ISR?
One key problem with SSG is if there is an update to data, that update won't be reflected unless the entire site is rebuilt. Imagine an e-commerce site with hundreds or thousands of products, when the npm run generate
command is run, data will be fetched from the database containing product information, and the pages are generated with that respective data.
If you had to update the title of a single product, or fix a typo in a product description, that change would not be reflected unless the entire site is rebuilt. This can quickly become a problem because if an e-commerce brand had thousands of products, this would be extremely ineffiecient and use a lot of build minutes for no reason. It simply isn't feasible to generate thousands of pages each time a piece of data needs to be changed.
If SSR would be used in this example, the data would always be up to date since for each request the page is generated on-demand every single time.
Although it might seem like SSR is a solution to this problem, there is a performance hit. Now we lose the performance of SSG since a small piece of data might change once in a while. It isn't feasible to lose the performance benefit of SSG if data changes once in a while, and at the same time, it isn't feasible to rebuild an entire site because a small piece of data changes once in a while.
ISR
ISR is the solution to this problem. When we run npm run build
we have a site that looks like it's SSR but it works differently when a request hits the server. The key difference is when a request hits the server, the page is generated on demand and cached, either for a set timeframe or with on-demand invalidation.
This means if an e-commerce site has thousands of products, the build time is short since each product page does not get prerendered. When a page is requested, the page is generated on-demand, cached, and subsequent requests can reuse that cached response.
Setup
In order to get a working example we need to create a folder netlify
at the root of our project. Shopify will be used for this example since it is a popular e-commerce solution.
- functions
- revalidate.js
netlify
must be used and not netlify_edge
because the durable
directive has no effect on responses from Netlify Edge Functions.import { purgeCache } from "@netlify/functions"
export default async (req, context) => {
const url = new URL(req.url)
const cacheTag = url.searchParams.get("tag")
if (!cacheTag)
return
await purgeCache({
tags: [cacheTag]
})
return new Response('cache purged', { status: 202 })
}
In the Shopify admin, a webhook can be created for the Product update
event with the following endpoint: /.netlify/functions/revalidate?tag=products
The last step to enable on-demand invalidation is to setup routeRules
in the Nuxt configuration. The Netlify-CDN-Cache-Control
header holds directives (instructions) for how the response should be cached on Netlify's CDN. The durable
directive tells Netlify to persist the response in permanent storage, and use that as the origin for any cache misses until it’s invalidated.
The Netlify-Cache-Tag
response header applies only to Netlify's CDN and is what enables on-demand invalidation. This allows us to invalidate specific cached objects associated with that tag even if the cache control headers indicates they're still fresh.
export default defineNuxtConfig({
routeRules: {
'/products/**': {
headers: {
'netlify-cache-tag': 'products',
'cache-control': 'public, max-age=0, must-revalidate',
'netlify-cdn-cache-control': 'public, max-age=3600, stale-while-revalidate=604800, durable'
}
}
}
})
Now any cached objects tagged with products
will be invalidated when purgeCache
is called with the products
tag.
If you pay attention you might have noticed that if an e-commerce site has thousands of products, our current implementation adds the products
tag to all product pages. Each time a single product is updated, all of the other cached objects tagged with products
will also be invalidated. It would be better to leave those alone and only invalidate the cached object for that specific page.
If you had a small amount of products, the routeRules
could be programmatically created inside of the nitro:config
hook.
defineNuxtConfig({
routeRules: {
'/products/**': {
headers: {
'netlify-cache-tag': 'products',
'cache-control': 'public, max-age=0, must-revalidate',
'netlify-cdn-cache-control': 'public, max-age=3600, stale-while-revalidate=604800, durable'
}
}
},
hooks: {
async 'nitro:config'(nitroConfig) {
const products = await getProducts({
variables: {
first: 100
}
})
const handles = products.map(({ handle }) => handle)
const routeRules = handles.reduce((acc, handle) => ({
...acc,
[`/products/${handle}`]: {
headers: {
'netlify-cache-tag': handle
}
}
}), {})
nitroConfig.routeRules = {
...nitroConfig.routeRules,
...routeRules
}
}
}
})
Each time a product is updated in the Shopify admin, the webhook will send a json payload to the enpoint we defined earlier. That json payload contains some information about the updated product including the product's handle.
import { purgeCache } from "@netlify/functions"
export default async (req, context) => {
const { handle } = await req.json()
await purgeCache({
tags: [handle]
})
return Response.json({ msg: 'purge successful' }, { status: 200 })
}
Instead of programmatically extending routeRules
within the Nitro nitro:config
hook, the preferred way to implement this would be to remove the Nitro hook from the config and set the Netlify-Cache-Tag
header directly on the product page.
<script setup>
import { setResponseHeader } from 'h3'
const { handle } = useRoute().params
const event = useRequestEvent()
if (import.meta.server) {
setResponseHeader(event, 'netlify-cache-tag', handle)
}
</script>
Conclusion
Using Nuxt on Netlify with ISR provides a powerful, scalable solution for building performant, modern web applications. By leveraging Nuxt’s server side rendering capabilities and Netlify’s seamless serverless deployment infrastructure, you can easily create dynamic, SEO-friendly pages with optimal loading speeds. ISR enhances this setup by enabling on-demand regeneration of static content, ensuring that users always see up-to-date content without requiring a full rebuild. The combination of Nuxt and Netlify ISR not only reduces build times but also minimizes backend server requirements, offering a streamlined and efficient approach to handling modern web applications. This setup is perfect for projects that require a balance between high performance and dynamic content.