导航与 Router Cache

Link 的 prefetch 默认行为、useRouter 的 push/replace/back/refresh,以及「为什么页面不刷新」的排查入口

Link 的 prefetch:默认就在偷偷干活

只在生产构建生效;dev 下每次点击都是完整请求

  • 静态路由:生产构建下,Link 进入视口即预取整个路由的 RSC 载荷并存入 Router Cache —— 用户点击时「秒开」的来源
  • 动态路由(运行时渲染的页面):只预取到最近的 loading.tsx 边界为止,即「骨架 + 静态壳」; 动态部分仍在点击后向服务器请求。这是设计而非缺陷: 预取一份注定过期的新鲜数据是浪费,预取壳则几乎免费
  • 推论:想要整页预取,就把页面静态化;动态页的「点击后转圈」 不是 prefetch 失效,是它本来就只承诺预取到 loading 边界

useRouter:四个动词各改什么

来自 next/navigation;只改 URL 与历史栈,不触发整页刷新

方法做什么典型场景
push导航并新增一条历史记录常规跳转,用户可用 back 退回
replace导航并替换当前历史记录登录后跳回、结算完成页 —— 不希望 back 回到中间态
back / forward沿历史栈前进后退自绘「返回」按钮;命中 Router Cache 时零请求
refresh丢弃当前路由的 Router Cache,重新向服务器取 RSC 载荷数据变了但 URL 没变时的「强制重取」旋钮

动手演练(可运行)

push / replace / back / refresh 与 prefetch 对照,均在下方真实执行

当前 pathname:/router/navigation

prefetch 对照(打开 DevTools Network,先清空,再滚动让链接进入视口)

生产构建下,默认 Link 进入视口就会预取目标路由的 RSC 载荷; prefetch= 的链接只有真正点击时才发请求 —— 两者目标相同,时机不同。

历史栈实验:先 push 一次再 back,能回到本页;先 replace 再 back, 本页这条记录已被替换,会直接退到更早的页面。

「为什么页面不刷新」:先怀疑 Router Cache

导航快是因为缓存在替你做主;觉得「脏」也是同一份缓存在做主

客户端导航命中 Router Cache 时,根本不会发请求,自然不会「刷新」。 这一层的生命周期、失效方式与四层缓存全景,在四层缓存对照台的「④ Router Cache」一节有完整对照与可运行演示;排查顺序 (先退客户端缓存,再查 Full Route Cache,最后查 Data Cache)也在该页。

staleTimes:Router Cache 的保质期(Next 15 行为变更)

「后退不重新请求,重新进入却重取」不是 bug,是 staleTimes 的设计

  • Next 15 起,Router Cache 里 page 段默认 staleTime = 0(14 时代是 30s):点击 Link 进入一个新页面,每次都会向服务器重取 RSC 载荷; 布局段与 prefetch 预取的载荷仍按 30s 缓存
  • 前进/后退不受 staleTimes 约束:back/forward 沿历史栈移动时, 页面一律从 Router Cache 还原(保滚动、保状态),这是浏览器式的「后退即现场」语义 —— 所以「后退秒开」与「重新进入重取」同时成立
  • 想让 page 段也缓存,在 next.config.ts 里显式调(示意,本站未开启):
// next.config.ts(示意)
const nextConfig = {
    experimental: {
        staleTimes: {
            dynamic: 30,  // page 段的 Router Cache 秒数;0 = 每次导航都重取(默认)
            static: 180,  // 预取静态路由后的缓存秒数
        },
    },
};

调大 dynamic 之前先想清楚:页面里有没有「用户刚改过就应该看到」的数据。 有的话保持 0,改数据的一方用 router.refresh() 或 revalidatePath 主动失效, 比全局拉长保质期安全。

什么时候别用 prefetch

预取省的是延迟,花的是服务器与用户的流量

  • 大数据量详情页:列表里几十个 Link 每个都预取, 等于用户没点就替他下载了几十个页面 —— 用 prefetch= 或 hover 时再预取
  • 低频页面:设置、帮助这类 99% 的会话不会进入的路由, 预取是纯浪费
  • 按流量计费的 API:预取会真实触发取数 (动态段至少触发到 loading 边界,静态页则是整份载荷), 后端按调用计费时要把这个放大系数算进成本
  • 决策要点:prefetch 是「赌用户会点」的投机加载; 点击率越低、单页越贵,越应该关掉它