
Next.js 16.3 把「点链接像 SPA」这件事正式推到了可开关的配置上:cacheComponents 加上 partialPrefetching。官方博客写得很直白——动态默认、缓存显式,导航不再靠猜。之前那篇写过 16.3 的内存回收和 AGENTS.md,这篇只谈导航:壳怎么预取、数据怎么后到、你怎么测「变慢了」。
参考:Next.js 16.3、Instant Navigations、示例仓 vercel-labs/next-beats。
为什么以前的导航会「顿一下」
App Router 把大量 UI 推到服务端,首屏 JS 少了,但路由切换常变成:等 RSC 载荷 → 再画。预取如果太狠,列表页一进视口就拉全量目标页,带宽和缓存都被拖累;太懒又像 MPA。
团队里常见抱怨就两类:
- 点了链接,白一下或骨架闪太久
- 开了 prefetch,首页在后台偷偷打出一串请求
我自己在后台类产品里踩过第二种:表格页挂了二十个「详情」链接,默认预取把接口 QPS 打成锯齿。关掉全局 prefetch 又回到「点一下等一下」。问题不在 React 慢,而在预取粒度和缓存合同没写清楚。
16.3 的答案不是「再隐式缓存一层」,而是让你写清楚:哪些是可复用的壳,哪些是带 tag 的数据。
打开 Instant Navigations
图:壳先提交,数据随后流式补上的导航体感

// next.config.ts
import type { NextConfig } from 'next'
const nextConfig: NextConfig = {
cacheComponents: true,
partialPrefetching: true,
}
export default nextConfig
两面一起开才有完整行为。cacheComponents 把 'use cache'、cacheTag、cacheLife 接到客户端导航链路;partialPrefetching 让链接进入视口时,优先预取共享 App Shell,导航先提交壳,数据再流式补上。
官方示范仓 next-beats 是个音乐播放器:列表进详情时,布局几乎立刻切过去,封面和曲目信息随后到。你本地对照它,比只读 changelog 快。听歌场景对「先出壳」特别敏感——用户要的是连续操作,不是完美像素同一帧到达。
壳和数据分开想
旧习惯是「整页要么 static 要么 dynamic」。新模型更像:
- Shell:导航栏、侧栏、列表骨架——可被 partial prefetch 提前拿到
- Cached data:带
cacheTag的查询,失效时按 tag 刷 - Live stream:必须实时的块继续用 Suspense,别堵在顶层 await 里
按产品页举例:
// app/products/[id]/page.tsx
import { cacheLife, cacheTag } from 'next/cache'
async function ProductTitle({ id }: { id: string }) {
'use cache'
cacheTag(`product-${id}`)
cacheLife('hours')
const p = await db.product.findUnique({ where: { id } })
return <h1>{p?.name}</h1>
}
export default async function Page({
params,
}: {
params: Promise<{ id: string }>
}) {
const { id } = await params
return (
<article>
<ProductTitle id={id} />
{/* 库存等易变块继续流式,不要和标题绑死 */}
</article>
)
}
标题可以缓存几小时,库存另开一块。用户从列表点进来时,至少标题区域有机会落在已预取的壳或缓存里,而不是整页一起等。
需要更深预取时,用 <Link prefetch> 拉更多目标内容;默认 partial 则偏保守。别一上来全站 prefetch={true},列表页会把自己打成爬虫。
写 cache tag 时我习惯和资源 ID 对齐:product-123、user-settings-9。失效从 mutation / webhook 里 revalidateTag,不要在「感觉数据旧了」时整站硬刷。tag 设计差,Instant Navigations 只会让你更快地展示过期壳。
迁移时会撞的墙
文档迁移指南里列过一批与 Cache Components 冲突的旧配置(dynamic、revalidate 等 segment 写法)。实践里我见过三类翻车:
顶层阻塞。 页面根上直接 await getSession() 再 return,壳出不来,partial prefetch 也救不了。把鉴权边界下沉,或拆成不阻塞外壳的分支。登录态可以用布局里的轻量 cookie 分支 + 内部再拉完整 session。
缓存粒度太大。 一个 'use cache' 包住整页,tag 一失效等于整页重算。按查询拆,tag 跟资源 ID 对齐。你若把「用户 + 推荐 + 广告」捆成一块缓存,任何一个字段变都得整包丢掉,壳策略再漂亮也白搭。
测不了「变慢」。 16.3 带了 @next/playwright 的 instant() 一类 helper,用来回归「这次重构有没有把导航拖慢」。没有自动化时,至少用官方 Instant Insights / Navigation Inspector 看慢路由,别只靠体感。把一两条核心路径的导航耗时写进 CI,比加一堆微交互动画值。
还有一类隐蔽问题:中间件重定向和国际化前缀。壳预取的是 A 路径,实际落地被 rewrite 到 B,用户会看到闪一下错布局。开 partial 之前,先把 canonical 路由和 redirect 表捋直。
和 View Transition 别混为一谈
昨天写过共享元素转场。View Transition 管的是视觉连续;Instant Navigations 管的是数据与预取策略。可以一起用,但排查时先分清:是动画卡,还是 RSC 还没到。先保证壳秒切,再谈 FLIP。
动效爱好者容易一上来全站 crossfade。对信息密度高的工具型页面,共享元素只留在「列表缩略图 → 详情主图」这种真正有空间对应的地方就够。导航策略错了,动画只是把等待包装得更丝滑,用户照样烦。
和 16.3 其它改动怎么一起看
同版本还有预取内联(小载荷合并请求)、SSR 换 Node stream、immutable 静态资源跨部署复用。它们和 Instant Navigations 是同一条线上的螺丝:少打请求、少阻塞、少把动态页当静态页骗自己。
升级时我建议顺序:先升 16.3 吃默认性能(内存、build cache),再开两个 flag 做一条列表→详情试点,最后才考虑全站 Cache Components 迁移。一口气改完,出了问题你分不清是 flag 还是业务代码。
我会怎么落
- 新项目或可接受 breaking 的分支:直接开两个 flag,对照 next-beats 搭一条列表→详情。
- 老项目:先给 1~2 条核心路径加
'use cache'+ tag,再开partialPrefetching,用 Playwright 钉住导航耗时。 - 观测:看预取请求数、导航到首次有意义绘制、tag 失效后的回流。数字比「感觉快了」管用。
- 文档:在仓库 AGENTS.md 或内部 RFC 里写清「哪些 tag 谁负责失效」,否则三个月后没人敢碰缓存。
带走一句: 16.3 的 Instant Navigations 不是特效开关,是把「壳先到、数据后到、缓存显式」写成默认心智。配置两行就能试;难的是拆缓存边界和别让顶层 await 堵死外壳。


