Advanced caching with Nuxt 4 on Netlify

by Philippe Serhal and Sean C Davis

Nuxt 4 hasn’t yet been fully released, but you can already opt into it on Netlify. Nuxt 4 on Netlify comes with out-of-the-box support for ISR, on-demand revalidation, and all other Netlify advanced caching primitives.

You can deliver high-performance sites while keeping your content up-to-date by using Nuxt with Netlify’s advanced cache control features. In this guide, we’ll explain how and why to use different caching options, and show the code you’ll need to get fine-grained control over caching behaviors in a Nuxt site opted into version 4 mode.

#What’s new in Nuxt 4?

Check out our Nuxt 4 announcement post for details on what’s new.

The major highlight for Netlify sites is that the full Netlify caching platform is unlocked. We’ll dig into how to make the most of it below.

#How to opt into Nuxt 4 early

To opt into Nuxt 4 before it’s fully released, follow the opt-in guide. In short, just set this:

nuxt.config.ts
export default defineNuxtConfig({
future: {
compatibilityVersion: 4,
},
});

To opt into all the Netlify improvements described in this guide, you also need to set your Nuxt compatibility date to 2024-05-07 or later (we recommend setting this to the current day):

nuxt.config.ts
export default defineNuxtConfig({
compatibilityDate: "2024-11-13",
future: {
compatibilityVersion: 4,
},
});

#How to upgrade to Nuxt 4

Most apps will be able to upgrade seamlessly, but be sure to follow the official 3 to 4 migration guide.

Then, deploy your site to Netlify as usual. Netlify automatically detects, builds, and deploys Nuxt sites with zero configuration. If you’ve manually configured your Nuxt 3 site, that configuration will work with Nuxt 4 as well.

#How to deploy a new Nuxt 4 site to Netlify

To deploy a new Nuxt site to Netlify or move an existing site to Netlify, you can link your repository to automatically build and deploy your app (recommended), or you can deploy manually from the command line.

Netlify CLI

Commands starting with netlify <...> use the powerful Netlify CLI. Install it by running:

Terminal window
npm i -g netlify-cli@latest

To link your site and deploy from the command line, first link your site:

Terminal window
netlify init

You can then deploy your site by running:

Terminal window
netlify deploy --build

Alternatively, you can link your repository in the Netlify UI to set up continuous deployment.

Try it

To get started with Nuxt on Netlify quickly, you can also clone and deploy a starter template for exploring and experimenting with just a few clicks. Use the button below to dive right in.

Deploy to Netlify

#What is ISR?

Incremental Static Regeneration or ISR is an approach to rendering that lets you skip rendering a given page at build time, and instead defer rendering it to the very first request, then always serve responses from the CDN edge cache and control when that page is regenerated in the background.

ISR was popularized by Next.js, which implements it internally within the framework, but it’s a pattern that can be implemented with any framework in a standards-based way using HTTP headers and a CDN that supports them.

With ISR, you can set a page to be revalidated as soon as a request is fulfilled with a stale cache entry (based on an etag or a time-based header like max-age). This revalidation occurs in the background without affecting your users.

In the case of Netlify, the CDN node will forward the request to the origin (a serverless function which renders the page with your site’s framework), and the response is cached on the CDN node and served on future requests. Clever CDNs will even deduplicate in-flight background revalidation requests for the same page.

#Why use ISR

These days, meta-frameworks like Nuxt support multiple rendering modes and even let you configure separate modes per route (Nuxt calls this hybrid rendering).

In any case, for a given route, you’re often faced with a choice between the following rendering options:

  1. Prerendering static HTML at build time, resulting in super fast load times from the CDN node closest to your users but requiring a full rebuild to update any content on your site.
  2. Server-side rendering (SSR) at request time, which can be slower but always serves the latest content, or uses time-based caching (i.e. max-age) to avoid re-rendering on every request.

Luckily you can have the best of both worlds if you use an advanced CDN: pages that are delivered quickly at runtime with fresh content from the edge cache, without triggering builds, not even incremental builds.

#How to implement ISR using Nuxt

A modern CDN like Netlify’s edge network gives you powerful tools to control your site’s cache using just HTTP headers. Nuxt lets you set rules on a page-by-page basis, so you can control how each page is cached and when it is regenerated.

When using universal rendering with Nuxt you can opt into ISR via routeRules:

nuxt.config.ts
export default defineNuxtConfig({
routeRules: {
"/blog/**": {
isr: 5 * 60,
},
},
});

The isr option tells the CDN to trigger a background revalidation for a route once a request comes in for that route after its cache has become stale (5 minutes after the last render, in the example above).

Other than on the very first request, the page load is never blocked for the user, though it won’t be perfectly fresh for everyone. Of course, you control exactly how much “staleness” is acceptable for each page. If the data is unlikely to change, you can set a long expiration time. Don’t worry if you find you need to change it later – any new deploy will clear the CDN cache instantly and your visitors will get the fresh content, without broken asset links.

If you’re just using this pattern to defer rendering static pages to save on build time, you can instruct the Netlify CDN to cache them indefinitely after the first request (don’t worry - the cache will be immediately invalidated on a new deploy or rollback!):

nuxt.config.ts
export default defineNuxtConfig({
routeRules: {
"/blog/**": { isr: true },
},
});

#How does ISR work?

Under the hood, responses are just separate cache headers for the Netlify CDN (Netlify-CDN-Cache-Control) and the browser (Cache-Control):

Cache-Control: public, max-age=0, must-revalidate
Netlify-CDN-Cache-Control: public, max-age=300, stale-while-revalidate=31536000, durable

Thanks to conditional requests, only content that has actually changed is downloaded by the browser.

The durable directive tells the CDN to also store responses in a secondary cache that is globally accessible to all edge nodes.

If you’ve opted in to deployment on Netlify Edge Functions, note that the durable directive is not yet supported. In this case, each node in the Netlify CDN only maintains its own independent cache.

This is all just syntactic sugar. If you prefer, you can set these headers manually and tweak anything to your heart’s content:

nuxt.config.ts
export default defineNuxtConfig({
routeRules: {
"/blog/**": {
headers: {
"Cache-Control": "public, max-age=60, must-revalidate",
"Netlify-CDN-Cache-Control": "public, max-age=600, stale-while-revalidate=31536000, durable",
},
},
},
});

#On-demand revalidation with cache tags

Up until now we’ve just been caching for a fixed amount of time. This is fine if you don’t know when a data source has updated, but if we can get notifications when it has changed, we can revalidate the cache for only the page where it is needed. This is because Netlify supports the Cache-Tag header, with fine-grained revalidation by tag. Netlify also supports the Netlify-Cache-Tag header with the same meaning but specific to just Netlify’s CDN, and which will not be visible to the browser. Most CMS and ecommerce platforms support webhooks to report content changes, so you can use this to trigger a revalidation of the cache for only the pages that need it.

To see how cache tags work, let’s look at an example. Cache tags let you mark each page with a tag that can then be used to invalidate the cache for any page with that tag. One of the most powerful uses for this is to tag a page with the IDs of the data that it uses. This means that when that data is changed, you can invalidate only the pages that use that data. This is a powerful way to do ISR, because you can keep the cache fresh for only the pages that need it, and not waste time re-rendering pages that haven’t changed. If you edit one entry in the CMS, the cache will be invalidated just for pages that use that data and not the whole site.

#How to revalidate your pages on demand with cache tags and webhooks

This example shows how to use Contentful webhooks to do on-demand revalidation, but you can use a similar approach with any CMS.

The first step is to tag each page with the data IDs. Here’s an example of how you might do this with Contentful:

<template>
<h1>{{ data.book.fields.title }}</h1>
<!-- etc. -->
</template>
<script setup lang="ts">
/* implementation of this plugin omitted for brevity */
import { createClient } from "~/plugins/contentful";
const client = createClient();
const data = await useAsyncData("fetchData", async (context) => {
const book = await client.getBookBySlug(context.params.slug):
if (!book) {
throw createError({statusCode: 404});
}
const {ssrContext} = useNuxtApp();
// Tag the page with the book slug if we're rendering on the server
if (ssrContext) {
ssrContext.res.setHeader("Netlify-Cache-Tag", slug);
}
return {book};
});
</script>

This sets the cache tag to the book slug, which in this example is the Contentful object ID. When the book is updated, the cache for this page is invalidated.

Next you can use a webhook to revalidate the cache for this page when the book is updated:

server/api/books/webhook.post.ts
import { purgeCache } from "@netlify/functions";
export default defineEventHandler(async (event) => {
const body = await readBody(event);
// Ignore illegitimate webhooks
const config = useRuntimeConfig();
if (event.headers.get("X-Contentful-Webhook-Secret") !== config.contentfulWebhookSecret) {
throw createError({ statusCode: 401 });
}
await purgeCache({ tags: [body.sys.id] });
setResponseStatus(event, 202);
});

This assumes you’ve populated your Contentful webhook secret as a CONTENTFUL_WEBHOOK_SECRET environment variable in your Netlify site and told Nuxt about it:

nuxt.config.ts
export default defineNuxtConfig({
runtimeConfig: {
contentfulWebhookSecret: process.env.CONTENTFUL_WEBHOOK_SECRET,
},
});

This uses the purgeCache function from the @netlify/functions package to invalidate the cache for all pages that reference that book. You can then set up a webhook in Contentful to call this webhook when a book is updated.

Next, create a webhook that posts to /api/books/webhook. Choose “Select specific triggering events” and choose “Entry”.

#Fine-grained cache control

So far we’ve been caching each page as the same for each visitor. This is probably what you want for most public pages, but not always. The Vary header can be used to store different versions of a page according to the request headers. However this can be quite an inflexible instrument, so Netlify extended this with the Netlify-Vary header.

An important case will be to control how query parameters are treated. The strict spec-compliant way that a cache will handle these is to treat the whole URL complete with query as the cache key, meaning that ?foo=bar and ?foo=baz will be treated as different pages. While this is what you want in some cases, there are a lot of times when this will break caching entirely. A very common example for this is social sharing. Links from social media will often have tracking parameters added automatically, and you don’t want to cache a different copy of the page for each visitor. This is also the case if you are using ads with tracking IDs. Should ?fbclid=nnnn... be stored as a separate page than ?fbclid=mmmm...? Probably not. However you might be using the query param for something that you do want to use for the cache.

A typical example might be something like sorting, filtering, or pagination. Netlify lets you control exactly which query params are used for the cache key using the Netlify-Vary header. Here’s how you can use it on a listings page:

nuxt.config.ts
export default defineNuxtConfig({
routeRules: {
"/books/**": {
headers: {
"Netlify-Vary": "query=sort|page|per_page",
},
},
},
});

In this example it will treat pages with different values for sort, page and per_page as different pages, but will ignore any other query params. This is a powerful tool for controlling your cache, and can be used in combination with cache tags for even finer control.

#Cookies and caching

If there are user preferences that change how a page is displayed they need not break caching. You probably shouldn’t vary on the whole Cookie header, because this will mean that every user need their own version of the page cached, but you can use the Netlify-Vary header to control exactly which cookies are used for the cache key. Here’s an example using a cookie to control the theme:

// nuxt.config.ts
export default defineNuxtConfig({
routeRules: {
"/**": {
headers: {
"Netlify-Vary": "cookie=theme",
},
},
},
});

You can use this to choose cookies with low cardinality (i.e. ones that have a small number of possible values) to vary the cache on, which means you can have customized pages while still caching them. If there is a light theme and a dark theme, each of these will then be cached separately. You shouldn’t use it for cookies that have high cardinality, like session or user IDs.

#Debugging your cache

Advanced cache tools like this can be tough to debug. The Cache-Status header can help, by showing whether the CDN had a had a hit, miss or stale response. Netlify supports this header, and you can use it to see what’s happening with your cache.

No caching in development

When running netlify dev it will strip all CDN cache headers, but the cache itself is not used. You need to deploy your site to see the cache in action.

Nuxt gives you powerful tools to control your cache when combined with a platform like Netlify that supports advanced caching features. This gives you the power to control your cache in a way that’s standards-based and uses the web platform.

#Get started

To create your own Nuxt site to deploy to Netlify, you can get started by running npx nuxi init <project-name>. When you’re ready, link your repository to Netlify to deploy automatically.

Or if you prefer, you can also deploy an example directly to Netlify from here by clicking the button below.

Deploy to Netlify

For more details see the Nuxt on Netlify docs.