默认情况下,路由是静态渲染的,同时数据请求会被缓存。
如下图,一个路由在 构建时 和 请求时 会有不同的缓存行为:
- 构建时 - 会重新设置全路由缓存 TODO
- 请求时 - 会先尝试客户端的路由缓存,若未命中则会向服务器重新请求。
请求记忆化
React 扩展了 fetch
API ,会自动记忆相同的 URL 及选项参数的请求结果。这意味着在 React 组件树中不同的位置去发起相同的请求,且这样的请求只会执行一次。
async function getItem() {
const res = await fetch('https://.../item/1');
return res.json();
}
const item = await getItem(); // 未命中缓存
const item = await getItem(); // 命中缓存
- 请求记忆化是 React 的特性,而非 Next.js ;
- 只会对
fetch
的GET
请求进行记忆化; - 记忆化只会用在 React 组件树上,意味着:
- 会应用在
generateMetadata
、generateStaticParmas
、layout
、page
和一些其他的服务端组件; - 不会应用在 Router Handler 中的
fetch
请求。
- 会应用在
- 一些无法用
fetch
获取的数据(比如数据库、GraphQL 等),可以用 React 的cache
函数来进行记忆化。
-
缓存的生命周期:从服务器接收到请求,直到 React 组件树被渲染完;
-
记忆化的数据不会在不同请求中共享,并只用于渲染,所以无需重新校验数据;
-
可以将
AbortController
的signal
传给fetch
来取消记忆化的行为。const { signal } = new AbortController(); fetch(url, { signal });
数据缓存
Next.js 扩展了 fetch
API 来进行数据缓存,它会对来自服务器上(包括部署)的数据请求结果进行持久化。
默认情况下 fetch
会进行数据缓存。可以用 fetch
中的 cache
或 next.revalidate
选项来自定义缓存行为。
浏览器中
fetch
的cache
选项参数用于控制浏览器中的 HTTP 缓存;在 Next.js 中,cache
用于设置服务器上数据缓存行为。
数据缓存是持久的,它不仅会应用于服务器发出的请求,还会应用于新的服务器部署过程中。
请求记忆化与数据缓存的区别:
- 请求记忆化中缓存的生命周期为单次(页面)请求;数据缓存是持久的。
- 请求记忆化旨在避免渲染时对同一接口的重复调用;数据缓存旨在减少对数据源的请求次数。
基于时间的重新校验
对缓存数据设定过期时间。通常用于变更频繁但实时性并不太重要的数据:
// 每一小时请求一次
fetch('https://...', { next: { revalidate: 3600 } });
或者也可以用路由片段配置来设置一个分段中所有 fetch
请求的配置。
按需的重新校验
可以通过路径(revalidatePath()
)或缓存标签(revalidateTag()
)来踢除缓存:
fetch('...', { next: { tags: ['a'] } }); // 未命中,请求后会缓存结果,并标记为 'a'
revalidateTag('a'); // 踢除了所有带有 'a' 标签的缓存
fetch('...', { next: { tags: ['a'] } }); // 仍未命中,请求后会缓存结果,并标记为 'a'
不使用数据缓存
对于个人数据,可以将 cache
设为 no-store
来取消缓存行为:
fetch('...', { cache: 'no-store' });
或者用路由片段配置:
// 会对所有请求起效,包括第三方库
export const dynamic = 'force-dynamic'
全路由缓存
Next.js 会在构建时对路由进行渲染及缓存。通过缓存,每次请求时就不必重新渲染页面。
- 在服务端,React 会根据路由片段和 Suspense 来拆分代码。通过流式渲染,可以不必等待整个页面完全渲染,而是一小块一小块地将页面内容(RSC Payload)发送给客户端。
- Next.js 会将上述的 RSC Payload 缓存起来,不管是构建时还是重新校验数据时。
- 在客户端上也会对 RSC Payload 进行缓存,称为 Router Cache ,它是一个根据路由片段划分的内存缓存。在后续的导航操作中,会先检查是否有对应的路由缓存,如果有则不再向服务端请求。
静态路由默认会有全路由缓存;而动态路由不会有全路由缓存,而是每次请求时重新渲染。
- 全路由缓存是持久的。
- 使全路由缓存失效的方法:
- 重新检验数据缓存;
- 重新进行项目部署(每次项目部署都会清除之前的额全路由缓存)。
- 取消全路由缓存的途径:
- 使用一个动态函数,如
cookies()
; - 设置路由片段配置:
dynamic = 'force-dynamic'
或revalidate = 0
; - 不使用数据缓存。对于
fetch
,如果没有数据缓存则也不会进行全路由缓存。
- 使用一个动态函数,如
路由缓存
在客户端中,Next.js 会在用户会话期间对 RSC Payload 进行缓存,同时是根据路由片段区分。路由缓存是记忆在浏览器内存中的。
路由缓存对于用户有这些体验提升:
- 立即响应的前进/后退导航切换 - 对于已访问的路由会直接使用路由缓存,而对于新路由会进行预先获取和部分渲染;
- 导航行为不会导致整页加载,同时 React 和浏览器状态都会得到保留。
路由缓存和全路由缓存的区别:
- 路由缓存仅在用户会话期间暂时保存,而全路由缓存在服务器上是持久的;
- 全路由缓存只能缓存静态路由,而路由缓存均可应用在动态静态路由上。
- 两个因素决定了路由缓存的生命周期:
- 会话 - 缓存会持久于导航行为中,而当页面刷新时缓存会一并清空。
- 自动过期 - 动态路由会在半分钟后过期,静态路由会在 5 分钟后过期。也可用
prefetch={true}
或router.prefetch
来使动态路由的过期时间变为 5 分钟。
- 使路由缓存失效的方法:
- (在 Server Action 中)重新校验数据缓存,或者使用
cookies.set
或cookies.get
方法; - 调用
router.refresh
向服务端重新请求当前路由。
- (在 Server Action 中)重新校验数据缓存,或者使用
- 没有办法能禁用路由缓存。
不同缓存间的交互
- 重新校验或者禁用数据缓存会导致全路由缓存失效,因为渲染内容的输出依赖于数据;
- 在 Route Handler 中重新校验了数据缓存并不会使路由缓存失效。
相关 API
这张表 列出了所有控制缓存行为的 API 。
<Link>
默认情况下,<Link>
会自动预请求全路由缓存,并将 RSC Payload 添加到客户端的路由缓存中。
可以设置 prefetch={false}
来取消自动预请求的行为,但不会影响访问路由后的路由缓存行为。
useRouter
router.prefetch
- 手动预请求路由,并将 RSC Payload 添加到客户端的路由缓存中。router.refresh
- 清除当前路由的路由缓存,并重新获取。会保留 React 和浏览器状态。
fetch
fetch
结果会自动添加到在数据缓存中:
// `force-cache` 是默认选项
fetch("https://...", { cache: 'force-cache' });
如果使用了 cache: 'no-store'
会跳过数据缓存,同时也会跳过全路由缓存。
动态函数
cookies
、headers
、searchParams
的时候会让路由动态渲染,因此不会有全路由缓存的行为。
在 Server Action 中使用 cookies.set
或 cookies.delete
会使路由缓存失效。
路由片段配置
当你不能使用 fetch
时(比如使用第三方库),可以通过路由片段设置来取消数据缓存及全路由缓存:
const dynamic = 'force-dynamic'
const revalidate = 0
generateStaticParams
对于动态片段(比如 app/blog/[slug]/page.js
)可以提供一个 generateStaticParams
,构建时会根据给定参数生成全路由缓存;在请求阶段时遇到的新路由也会做缓存。
如果要拒绝请求阶段遇到的新参数,可用 const dynamicParams = false
配置,当遇到新参数时会返回 404 。
React cache
如果不使用 fetch
请求数据,也可用 React 的 cache
函数来实现请求记忆化:
import { cache } from 'react'
import db from '@/lib/db'
export const getItem = cache(async (id) => {
const item = await db.item.findUnique({ id });
return item;
})