0Pricing
Next.js 15 Fullstack (App Router + Server Actions) · 课时

基于时间与按需重新验证的策略

结合重新验证间隔、revalidatePath 和 revalidateTag,精确控制数据新鲜度。

基于时间与按需重新验证的策略 是 CoddyKit 上的免费 Next.js 15 Fullstack (App Router + Server Actions) 课时。 这是第 2 节课,共 4 节。 你可以在下方免费阅读本课时的完整内容 — 然后在浏览器中使用内置代码编辑器和全天候 AI 导师进行实践。 这是 Next.js 15 Fullstack (App Router + Server Actions) 学习路径的一部分,你的进度在网页和 CoddyKit 应用中同步。 Next.js 15 Fullstack (App Router + Server Actions) 课程共包含 4 节课。

本课时的部分内容尚未翻译,以英文显示。

Two Axes of Freshness

In the App Router, a cached route or fetch can be refreshed along two independent axes:

  • Time-based revalidation: data goes stale after a fixed number of seconds (revalidate).
  • On-demand revalidation: you explicitly invalidate cache when something actually changes, using revalidatePath or revalidateTag.

Real apps combine both. Time-based gives a safety net so content never gets too old; on-demand gives instant updates when you control the write. This lesson shows how to wire them together for precise freshness control.

Time-Based with the Segment Config

The simplest time-based strategy is the route segment revalidate export. Next.js will serve the cached render until the interval elapses, then regenerate it in the background (stale-while-revalidate).

A value of 3600 means "at most one hour old". Setting 0 opts out of caching for the segment; false caches indefinitely.

// app/products/page.tsx
// Revalidate this Server Component page at most once per hour
export const revalidate = 3600;

export default async function ProductsPage() {
  const res = await fetch('https://api.example.com/products');
  const products: { id: string; name: string }[] = await res.json();

  return (
    <ul>
      {products.map((p) => (
        <li key={p.id}>{p.name}</li>
      ))}
    </ul>
  );
}

Per-Fetch revalidate

You do not have to revalidate a whole segment. Each fetch can carry its own time-based policy through the next.revalidate option.

This lets one page mix a fast-changing resource with a slow-changing one. The segment's effective revalidate becomes the lowest value among its fetches and its segment config.

// app/dashboard/page.tsx
export default async function Dashboard() {
  // Prices change often: refresh every 60s
  const prices = await fetch('https://api.example.com/prices', {
    next: { revalidate: 60 },
  }).then((r) => r.json());

  // Categories rarely change: refresh once a day
  const categories = await fetch('https://api.example.com/categories', {
    next: { revalidate: 86400 },
  }).then((r) => r.json());

  return <Report prices={prices} categories={categories} />;
}

Why Time Alone Is Not Enough

Time-based revalidation has a structural weakness: it cannot tell whether anything actually changed. With revalidate = 3600:

  • A user who edits a product still sees the old version for up to an hour.
  • If nothing changed, you regenerate anyway, wasting compute.

The fix is to pair a generous time interval (the safety net) with on-demand invalidation triggered by your own writes. Time keeps data from going truly stale; on-demand makes intentional edits feel instant.

revalidatePath: Invalidate by Route

revalidatePath purges the cache for a specific route so the next request re-renders it. Call it from a Server Action or Route Handler after a write.

  • Pass an exact path like '/products'.
  • Pass a dynamic template plus 'page' to target one dynamic route: revalidatePath('/products/[id]', 'page').
  • Pass 'layout' to invalidate a layout and everything nested under it.
'use server';
import { revalidatePath } from 'next/cache';
import { db } from '@/lib/db';

export async function updateProduct(id: string, name: string) {
  await db.product.update({ where: { id }, data: { name } });

  // Refresh the list and this product's detail page
  revalidatePath('/products');
  revalidatePath('/products/[id]', 'page');
}

revalidateTag: Invalidate by Data

revalidatePath is route-centric, but the same data can appear on many routes. revalidateTag is data-centric: you tag fetches, then invalidate every cached entry sharing that tag, no matter where it is rendered.

First, attach tags when fetching:

// lib/products.ts
export async function getProducts() {
  const res = await fetch('https://api.example.com/products', {
    next: { tags: ['products'], revalidate: 3600 },
  });
  return res.json();
}

export async function getProduct(id: string) {
  const res = await fetch(`https://api.example.com/products/${id}`, {
    next: { tags: ['products', `product:${id}`] },
  });
  return res.json();
}

Triggering revalidateTag from a Write

With tags in place, one call invalidates everything tagged. A coarse tag like 'products' refreshes lists, grids, and detail pages at once; a granular tag like product:42 refreshes only that item's cached entries.

Use both: invalidate the granular tag for the edited row, plus the collection tag for any aggregate views.

'use server';
import { revalidateTag } from 'next/cache';
import { db } from '@/lib/db';

export async function editProduct(id: string, name: string) {
  await db.product.update({ where: { id }, data: { name } });

  revalidateTag(`product:${id}`); // this item's detail entries
  revalidateTag('products');      // any list/grid that shows it
}

Path vs Tag: Choosing

How to decide between the two on-demand tools:

  • Use revalidatePath when you know exactly which route(s) changed and the mapping is simple, e.g. a CMS page slug.
  • Use revalidateTag when one piece of data fans out across many routes, or when routes are hard to enumerate (search pages, related-items widgets, sitemaps).

They are not exclusive. A common pattern: tag the underlying fetches for data fan-out, and additionally call revalidatePath for a high-traffic landing page you want refreshed immediately.

Combining Time + On-Demand

The production recipe is layered:

  • Set a generous time interval on tagged fetches (revalidate: 3600) as a backstop for changes you do not control.
  • Call revalidateTag in every Server Action that writes that data for instant updates you do control.

Result: edits appear immediately, while external drift can never exceed one hour. This is the sweet spot for "precise freshness control".

'use server';
import { revalidateTag } from 'next/cache';
import { db } from '@/lib/db';

// Fetch elsewhere uses: next: { tags: ['orders'], revalidate: 3600 }
export async function createOrder(input: { sku: string; qty: number }) {
  const order = await db.order.create({ data: input });
  revalidateTag('orders'); // instant; time interval is the backstop
  return order.id;
}

On-Demand via a Webhook Route Handler

When data changes outside your app (a headless CMS publish, an upstream sync), trigger revalidation from a Route Handler that the external system calls. Protect it with a secret so it cannot be abused.

// app/api/revalidate/route.ts
import { NextRequest, NextResponse } from 'next/server';
import { revalidateTag } from 'next/cache';

export async function POST(req: NextRequest) {
  const secret = req.nextUrl.searchParams.get('secret');
  if (secret !== process.env.REVALIDATE_SECRET) {
    return NextResponse.json({ message: 'Invalid token' }, { status: 401 });
  }

  const { tag } = await req.json();
  revalidateTag(tag);
  return NextResponse.json({ revalidated: true, now: Date.now() });
}

Gotchas to Remember

A few behaviors that trip people up:

  • revalidatePath/revalidateTag only mark the cache as stale; regeneration happens on the next request, not instantly during your action.
  • They must run on the server (Server Action or Route Handler), never in a Client Component.
  • A fetch with cache: 'no-store' or revalidate: 0 is never cached, so tags on it have nothing to invalidate.
  • Tags attach to fetch data caching; pick stable, predictable tag names so writers and readers agree.

Quick Check

Pick the most precise strategy for the scenario below.

Recap

You combined the two freshness axes for precise control:

  • Time-based: segment export const revalidate or per-fetch next: { revalidate } as a backstop against uncontrolled drift.
  • On-demand by route: revalidatePath when you know exactly which route changed.
  • On-demand by data: revalidateTag when one resource fans out across many routes.

The production pattern is a generous time interval on tagged fetches plus revalidateTag in every write Server Action (and a secret-protected webhook for external changes). Edits feel instant; staleness is bounded.

常见问题解答

「基于时间与按需重新验证的策略」课时是免费的吗?

是的 — 「基于时间与按需重新验证的策略」的完整文本可在网页上免费阅读。要进行交互式练习(内置代码编辑器和全天候 AI 导师)并解锁 Next.js 15 Fullstack (App Router + Server Actions) 课程的其余内容,请升级到 CoddyKit PRO。 Next.js 15 Fullstack (App Router + Server Actions) 课程共包含 4 节课。

「基于时间与按需重新验证的策略」这节课中我会学到什么?

结合重新验证间隔、revalidatePath 和 revalidateTag,精确控制数据新鲜度。 你通过在浏览器中直接运行的动手代码来练习 Next.js 15 Fullstack (App Router + Server Actions),全天候 AI 导师会在你学习这节课的过程中回答你的问题。

学习 Next.js 15 Fullstack (App Router + Server Actions) 需要有经验吗?

无需任何先前经验。CoddyKit 上的 Next.js 15 Fullstack (App Router + Server Actions) 课程适合初学者到高级学习者,你可以从这里开始或从头开始,按照自己的节奏学习。 这是第 2 节课,共 4 节。

「基于时间与按需重新验证的策略」课时需要多长时间?

大多数 CoddyKit 课程大约需要 5–10 分钟。每节课都很精短且互动,所以你能稳步进步,并在网页和应用中从离开的地方继续。

我能在这节 Next.js 15 Fullstack (App Router + Server Actions) 课中编写并运行代码吗?

能。每节 Next.js 15 Fullstack (App Router + Server Actions) 课都包含内置代码编辑器,你可以在浏览器中直接编写并运行真实代码,并获得即时 AI 反馈 — 无需本地设置。

此课程中的所有课时

  1. 四种缓存:请求、数据、完整路由与路由器
  2. 基于时间与按需重新验证的策略
  3. 基于标签的精细缓存失效
  4. 退出缓存:动态渲染与 no-store
← 返回 Next.js 15 Fullstack (App Router + Server Actions)