Question
For sites with thousands of pages, revalidating every URL after a global content change is expensive. Is there a way to just invalidate the cache — the way a Varnish BAN works — and then rebuild only the pages that matter?
Where the cost actually is
It is worth separating two things this codebase currently conflates:
|
What it does |
Cost |
| Invalidation |
Mark cache entries stale. revalidateTag() / revalidatePath() |
O(1), cheap |
| Warming |
Force pages to re-render eagerly so the first visitor doesn't pay |
O(pages), expensive |
The expensive part today is warming, and it is deliberate:
// next/src/app/api/revalidate/route.ts
revalidatePath(path); // Only purges the cache
const nextUrl = process.env.NEXT_URL || request.nextUrl.origin;
await fetch(`${nextUrl}${path}`); // We need to simulate the 1st request to put the new
// response in the cache (so the 1st user gets the new
// cached response)
…and on the WordPress side, nextjs-revalidate enqueues one URL per post and issues a wp_remote_get per item with a 60 s timeout (include/Revalidate.php:123, include/RevalidateQueue.php).
Next.js already gives us cheap invalidation with lazy, stale-while-revalidate regeneration. So the design question is not "how do we invalidate cheaply" — it's "which pages deserve warming, and how do we express that?"
Open questions
- Does tag invalidation propagate to the full-route cache? When a cached function tagged
fse-templates is consumed during a page render, does revalidateTag('fse-templates') invalidate the already-prerendered route cache entries for pages that used it, or only the data-cache entry? This determines whether a single tagged ping is sufficient, or whether path revalidations are still needed on top. Currently unverified — needs proving against tipee.ch staging, not assumed.
- What is the right warming policy? Candidates: warm nothing (accept a slow first hit); warm a small allowlist (home, key landing pages); warm the top-N by traffic; warm on a trickle in the background rather than in a burst.
- Should warming move out of the revalidate endpoint entirely? It is currently an implementation detail of a request whose job is invalidation. A separate, rate-limited warmer would decouple the two and stop a bulk invalidation from turning into thousands of synchronous fetches.
- How does this interact with
export const revalidate = 3600 (next/src/app/[[...uri]]/page.tsx:19) and with ISR's own stale-while-revalidate behaviour? Some of the warming may be redundant with what ISR already does.
- Multi-instance invalidation. On self-hosted deployments,
revalidateTag propagates through the filesystem cache in .next/cache, shared by PM2 workers on the same host. Any move to more than one host behind a load balancer requires a shared cache handler. Current deployments are single-host SSH+PM2 or Vercel, so this is not a problem today — but it bounds the design.
Origin
Came out of a design session on regenerating the FSE snapshot when WordPress FSE content changes. That work moves the snapshot from a build artifact to runtime cached data invalidated by a cache tag, and the WordPress-side triggers are specified in superhuit-agency/nextjs-revalidate#30 (with superhuit-agency/nextjs-revalidate#29 and superhuit-agency/nextjs-revalidate#28 as prerequisites).
Question 1 above is a hard acceptance criterion for that work — it is filed here rather than there because the answer belongs to the frontend, and because questions 2–5 outlive the FSE feature.
Measurements for reference
From tipee.ch against admin-staging.tipee.ch (702 KB snapshot, 13 templates, 3 template parts, 1163 blocks):
network (2 GraphQL queries, parallel) = 889–1045 ms
parse + format (formatBlocksJSON) = 12–31 ms
JSON.parse(702 KB) = 1.8–2.7 ms
full build script, cold = 2.13 s wall
Question
For sites with thousands of pages, revalidating every URL after a global content change is expensive. Is there a way to just invalidate the cache — the way a Varnish
BANworks — and then rebuild only the pages that matter?Where the cost actually is
It is worth separating two things this codebase currently conflates:
revalidateTag()/revalidatePath()The expensive part today is warming, and it is deliberate:
…and on the WordPress side,
nextjs-revalidateenqueues one URL per post and issues awp_remote_getper item with a 60 s timeout (include/Revalidate.php:123,include/RevalidateQueue.php).Next.js already gives us cheap invalidation with lazy, stale-while-revalidate regeneration. So the design question is not "how do we invalidate cheaply" — it's "which pages deserve warming, and how do we express that?"
Open questions
fse-templatesis consumed during a page render, doesrevalidateTag('fse-templates')invalidate the already-prerendered route cache entries for pages that used it, or only the data-cache entry? This determines whether a single tagged ping is sufficient, or whether path revalidations are still needed on top. Currently unverified — needs proving against tipee.ch staging, not assumed.export const revalidate = 3600(next/src/app/[[...uri]]/page.tsx:19) and with ISR's own stale-while-revalidate behaviour? Some of the warming may be redundant with what ISR already does.revalidateTagpropagates through the filesystem cache in.next/cache, shared by PM2 workers on the same host. Any move to more than one host behind a load balancer requires a shared cache handler. Current deployments are single-host SSH+PM2 or Vercel, so this is not a problem today — but it bounds the design.Origin
Came out of a design session on regenerating the FSE snapshot when WordPress FSE content changes. That work moves the snapshot from a build artifact to runtime cached data invalidated by a cache tag, and the WordPress-side triggers are specified in superhuit-agency/nextjs-revalidate#30 (with superhuit-agency/nextjs-revalidate#29 and superhuit-agency/nextjs-revalidate#28 as prerequisites).
Question 1 above is a hard acceptance criterion for that work — it is filed here rather than there because the answer belongs to the frontend, and because questions 2–5 outlive the FSE feature.
Measurements for reference
From tipee.ch against
admin-staging.tipee.ch(702 KB snapshot, 13 templates, 3 template parts, 1163 blocks):