增量静态再生成(ISR)
增量静态再生成(Incremental Static Regeneration,ISR)让你从 CDN 提供静态生成的内容,同时在后台定期重新生成。这样既能获得静态站点的性能优势,又能获得动态内容的新鲜度。
ISR 在 TanStack Start 中如何工作
TanStack Start 的 ISR 方案很灵活,它利用适用于任何 CDN 的标准 HTTP 缓存头。与框架专属的 ISR 实现不同,这种方案让你在页面和数据两个层面都能完全控制缓存行为。
核心概念很简单:
- 静态预渲染:页面在构建时生成
- CDN 缓存:缓存头控制 CDN 缓存 HTML 多长时间
- 重新验证:缓存过期后,下一个请求触发重新生成
- 过期再验证(Stale-While-Revalidate):在后台获取新数据的同时,先提供过期内容
译者注:ISR 与 SSG/SSR 的区别
ISR 常被拿来和 SSG、SSR 对比:SSG 构建一次就永远不变(更新要重新构建);SSR 每个请求都实时渲染(最新但最慢);ISR 介于两者之间——首屏是构建时的静态 HTML(快、便宜),到期后 CDN 上的下一次请求会触发后台重新生成。TanStack Start 没有「专有 ISR 机制」,它靠标准 Cache-Control 头让 CDN 实现同样的效果,因此与任何 CDN 都兼容。
缓存头策略
基于时间的重新验证
最常见的 ISR 模式是在 Cache-Control 头中使用 max-age 和 s-maxage 指令:
import { tanstackStart } from '@tanstack/react-start/plugin/vite'
import { defineConfig } from 'vite'
export default defineConfig({
plugins: [
tanstackStart({
prerender: {
routes: ['/blog', '/blog/posts/*'],
crawlLinks: true,
},
}),
],
})// routes/blog/posts/$postId.tsx
import { createFileRoute } from '@tanstack/react-router'
export const Route = createFileRoute('/blog/posts/$postId')({
loader: async ({ params }) => {
const post = await fetchPost(params.postId)
return { post }
},
headers: () => ({
// Cache at CDN for 1 hour, allow stale content for up to 1 day
'Cache-Control':
'public, max-age=3600, s-maxage=3600, stale-while-revalidate=86400',
}),
})
export default function BlogPost() {
const { post } = Route.useLoaderData()
return (
<article>
<h1>{post.title}</h1>
<div>{post.content}</div>
</article>
)
}理解 Cache-Control 指令
public:任何缓存都可以缓存该响应(CDN、浏览器等)max-age=3600:内容在 3600 秒(1 小时)内是新鲜的s-maxage=3600:为共享缓存(CDN)覆盖max-agestale-while-revalidate=86400:在后台重新验证的同时,最多 24 小时内提供过期内容immutable:内容永不变化(用于基于哈希的静态资源)
用服务器函数做 ISR
服务器函数也可以为动态数据端点设置缓存头:
// routes/api/products/$productId.ts
import { createFileRoute } from '@tanstack/react-router'
export const Route = createFileRoute('/api/products/$productId')({
server: {
handlers: {
GET: async ({ params, request }) => {
const product = await db.products.findById(params.productId)
return Response.json(
{ product },
{
headers: {
'Cache-Control':
'public, max-age=300, stale-while-revalidate=600',
'CDN-Cache-Control': 'max-age=3600', // Cloudflare-specific
},
},
)
},
},
},
})用中间件设置缓存头
对于 API 路由,可以用中间件设置缓存头:
// routes/api/products/$productId.ts
import { createFileRoute } from '@tanstack/react-router'
import { createMiddleware } from '@tanstack/react-start'
const cacheMiddleware = createMiddleware().server(async ({ next }) => {
const result = await next()
// Add cache headers to the response
result.response.headers.set(
'Cache-Control',
'public, max-age=3600, stale-while-revalidate=86400',
)
return result
})
export const Route = createFileRoute('/api/products/$productId')({
server: {
middleware: [cacheMiddleware],
handlers: {
GET: async ({ params }) => {
const product = await db.products.findById(params.productId)
return Response.json({ product })
},
},
},
})对于页面路由,直接用 headers 属性更简单:
// routes/blog/posts/$postId.tsx
import { createFileRoute } from '@tanstack/react-router'
export const Route = createFileRoute('/blog/posts/$postId')({
loader: async ({ params }) => {
const post = await fetchPost(params.postId)
return { post }
},
headers: () => ({
'Cache-Control': 'public, max-age=3600, stale-while-revalidate=86400',
}),
})按需重新验证
虽然基于时间的重新验证在大多数情况下表现良好,但你可能需要立即使特定页面失效(比如内容被更新时):
// routes/api/revalidate.ts
import { createFileRoute } from '@tanstack/react-router'
export const Route = createFileRoute('/api/revalidate')({
server: {
handlers: {
POST: async ({ request }) => {
const { path, secret } = await request.json()
// Verify secret token
if (secret !== process.env.REVALIDATE_SECRET) {
return Response.json({ error: 'Invalid token' }, { status: 401 })
}
// Trigger CDN purge via your CDN's API
await fetch(
`https://api.cloudflare.com/client/v4/zones/${ZONE_ID}/purge_cache`,
{
method: 'POST',
headers: {
Authorization: `Bearer ${CF_API_TOKEN}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
files: [`https://yoursite.com${path}`],
}),
},
)
return Response.json({ revalidated: true })
},
},
},
})CDN 专属配置
Cloudflare Workers
Cloudflare 遵循标准的 Cache-Control 头,并提供额外的控制:
export const Route = createFileRoute('/products/$id')({
headers: () => ({
'Cache-Control': 'public, max-age=3600',
// Cloudflare-specific header for finer control
'CDN-Cache-Control': 'max-age=7200',
}),
})Netlify
Netlify 使用 Cache-Control 头,也支持 _headers 文件:
# public/_headers
/blog/*
Cache-Control: public, max-age=3600, stale-while-revalidate=86400
/api/*
Cache-Control: public, max-age=300Vercel
部署到 Vercel 时,使用它们的 Edge Network 缓存头:
export const Route = createFileRoute('/posts/$id')({
headers: () => ({
'Cache-Control': 'public, s-maxage=3600, stale-while-revalidate=86400',
}),
})把 ISR 与客户端缓存结合
TanStack Router 的内置缓存控制可以与 CDN 缓存协同工作:
export const Route = createFileRoute('/posts/$postId')({
loader: async ({ params }) => {
return fetchPost(params.postId)
},
// CDN caching (via headers)
headers: () => ({
'Cache-Control': 'public, max-age=3600, stale-while-revalidate=86400',
}),
// Client-side caching (via TanStack Router)
staleTime: 60_000, // Consider data fresh for 60 seconds on client
gcTime: 5 * 60_000, // Keep in memory for 5 minutes
})这形成了一个多层级缓存策略:
- CDN 边缘:1 小时缓存,过期再验证 24 小时
- 客户端:60 秒新鲜数据,内存保留 5 分钟
常见 ISR 模式
博客文章
export const Route = createFileRoute('/blog/$slug')({
loader: async ({ params }) => fetchPost(params.slug),
headers: () => ({
// Cache for 1 hour, allow stale for 7 days
'Cache-Control': 'public, max-age=3600, stale-while-revalidate=604800',
}),
staleTime: 5 * 60_000, // 5 minutes client-side
})电商产品页
export const Route = createFileRoute('/products/$id')({
loader: async ({ params }) => fetchProduct(params.id),
headers: () => ({
// Shorter cache due to inventory changes
'Cache-Control': 'public, max-age=300, stale-while-revalidate=3600',
}),
staleTime: 30_000, // 30 seconds client-side
})营销落地页
export const Route = createFileRoute('/landing/$campaign')({
loader: async ({ params }) => fetchCampaign(params.campaign),
headers: () => ({
// Long cache for stable content
'Cache-Control': 'public, max-age=86400, stale-while-revalidate=604800',
}),
staleTime: 60 * 60_000, // 1 hour client-side
})用户专属页面
export const Route = createFileRoute('/dashboard')({
loader: async () => fetchUserData(),
headers: () => ({
// Private cache, no CDN caching
'Cache-Control': 'private, max-age=60',
}),
staleTime: 30_000,
})最佳实践
1. 从保守开始
先用较短的缓存时间,随着你了解自己的内容更新模式再逐步加长:
// Start here
'Cache-Control': 'public, max-age=300, stale-while-revalidate=600'
// Then move to
'Cache-Control': 'public, max-age=3600, stale-while-revalidate=86400'2. 用 ETag 重新验证
ETag 帮助 CDN 高效地重新校验内容:
import { createMiddleware } from '@tanstack/react-start'
import crypto from 'crypto'
const etagMiddleware = createMiddleware().server(async ({ next }) => {
const result = await next()
// Generate ETag from response content
const etag = crypto
.createHash('md5')
.update(JSON.stringify(result.data))
.digest('hex')
result.response.headers.set('ETag', `"${etag}"`)
return result
})3. 按查询参数区分缓存
当内容因查询参数而变化时,把它们纳入缓存 key:
export const Route = createFileRoute('/search')({
headers: () => ({
'Cache-Control': 'public, max-age=300',
Vary: 'Accept, Accept-Encoding',
}),
})4. 监控缓存命中率
追踪 CDN 性能以优化缓存时间:
const cacheMonitoringMiddleware = createMiddleware().server(
async ({ next }) => {
const result = await next()
// Log cache status (from CDN headers)
console.log('Cache Status:', result.response.headers.get('cf-cache-status'))
return result
},
)5. 与静态预渲染结合
在构建时预渲染以获得即时首屏,然后用 ISR 处理更新:
import { tanstackStart } from '@tanstack/react-start/plugin/vite'
import { defineConfig } from 'vite'
export default defineConfig({
plugins: [
tanstackStart({
prerender: {
routes: ['/blog', '/blog/posts/*'],
crawlLinks: true,
},
}),
],
})调试 ISR
检查缓存头
用浏览器 DevTools 或 curl 检查缓存头:
curl -I https://yoursite.com/blog/my-post
# Look for:
# Cache-Control: public, max-age=3600, stale-while-revalidate=86400
# Age: 1234 (time in cache)
# X-Cache: HIT (from CDN)测试重新验证
强制缓存未命中来测试重新生成:
# Cloudflare: Bypass cache
curl -H "Cache-Control: no-cache" https://yoursite.com/page
# Or use CDN-specific cache purge APIs监控性能
追踪关键指标:
- 缓存命中率:由缓存直接响应的请求所占的百分比
- 重新验证时间:重新生成过期内容所需时间
- 首字节时间(TTFB):缓存内容应该很低
相关资源
- 静态预渲染——构建时的页面生成
- 托管——CDN 部署配置
- 服务器函数——创建动态数据端点
- TanStack Router 数据加载——客户端缓存控制
- 中间件——请求/响应定制