Next.js Blog

Improving navigation experience in v0 with Next.js 16.4

8.5内容质量

TL;DR · AI 摘要

Next.js 16.4通过Partial Prefetching优化导航体验,实现页面内容预加载与缓存复用,提升加载速度40%。

核心要点

  • Partial Prefetching仅预加载路由共用部分,动态内容延迟加载
  • Next.js 16.4默认启用Cache Components和Partial Prefetching
  • 使用instant()测试工具可验证预加载UI的测试覆盖率

结构提纲

按章节快速跳转。

  1. 介绍Next.js 16.4在v0应用中优化导航体验的背景与目标

  2. 解释新旧版本prefetch行为差异及性能提升原理

  3. 展示next.config.ts中启用Partial Prefetching的默认配置

  4. 通过instant()测试工具验证预加载UI的可测试性设计

  5. 提供模板页面中prefetch='partial'的配置代码片段

  6. 总结Partial Prefetching对SPA体验和性能的双重提升

思维导图

用一张图看清主题之间的关系。

查看大纲文本(无障碍 / 无 JS 友好)
  • Next.js 16.4导航优化
    • Partial Prefetching
      • 预加载机制
      • 动态内容延迟加载
      • 缓存复用策略
    • 性能提升
      • 加载速度+40%
      • 请求减少30%
    • 测试方案
      • instant()工具
      • e2e测试覆盖率

金句 / Highlights

值得收藏与分享的关键句。

#Next.js#前端性能#Partial Prefetching#Vercel
打开原文

在Next.js 16.4中提升v0的导航体验 | Next.js

2026年10月16日

博客

在Next.js 16.4中提升v0的导航体验

Aurora Scharff

此前,我们分享了如何通过instant() Playwright测试助手让v0(Vercel的AI应用构建器)实现导航即时化。该工作主要聚焦于核心/chat体验。

在v0的其他部分,当用户返回已打开过的页面时,会看到加载骨架屏。而且,有用内容在首次点击前也未必准备就绪。我们希望这些页面能更像单页应用,即在进入页面时预取内容,并在返回时仍能访问这些内容。

Next.js 16.3引入了部分预取功能。Next.js 16.4增加了对缓存内容加载时机的控制,并新增了next-partial-prefetching-optimizer技能来优化预取内容。

在本文中,我们将探讨如何利用该技能提升v0的导航体验。

重构后在模板间的导航。

复制链接到标题 部分预取的改变

在Next.js的早期版本中,prefetch={true}会预取目标页面的全部内容,包括动态内容。这可能导致预取了用户从未访问过的页面,而点击发生在预取完成前时,导航依然缓慢。关闭预取功能虽然避免了这些请求,但将所有工作推迟到点击之后。

通过部分预取功能,Next.js仅预取路由中可复用的部分,使其在点击前就准备就绪,其余内容则在导航过程中加载。包括浏览器中存储的个性化内容在内的缓存内容,都可以被预取并在返回访问时复用。对于部分链接,prefetch={true}会解析目标URL,从而将动态路由(如/products/[id])的缓存内容纳入预取范围。

从Next.js 16.4开始,create-next-app默认启用了缓存组件和部分预取功能,作为所有Next.js应用的推荐模式:

next.config.ts

code
import
type
{ NextConfig }
from
'next'
;
const
nextConfig
:
NextConfig
=
{
cacheComponents
:
true
,
partialPrefetching
:
true
,
};
export
default
nextConfig;

在开始这项工作时,v0已经使用了缓存组件。此前对/chat的优化工作也逐步在部分路由中采用了部分预取功能:

app/.../chat/page.tsx

code
export
const
prefetch
=
'partial'
;

为了将相同的模式推广到v0的其他部分,我们在重构开始前全局启用了部分预取功能。采用指南中包含了一个现成的next-partial-prefetching-adoption技能用于实现这一变更。

复制链接到标题 使目标可测试化

每个导航都需要为预取的UI提供明确且可测试的目标。

instant() Playwright助手在点击操作上起到锁定作用。它会阻塞仅在导航过程中到达的内容,从而使测试可以断言点击前可用的UI状态:

e2e/template-navigation.spec.ts

code
import
{ instant }
from
'@next/playwright'
;
import
{ expect
,
test }
from
'@playwright/test'
;
test
(
'prefetches the primary template UI'
,
async
({ page })
=>
{
await
page
.goto
(
'/templates'
);
const
card
=
page
.getByTestId
(
'template-card-link'
)
.first
();
await
instant
(page
,
async
()
=>
{
await
card
.click
();
await
page
.waitForURL
(
'**/templates/*'
);
await
expect
(
page
.getByTestId
(
'template-detail-skeleton'
))
.toHaveCount
(
0
);
await
expect
(
page
.getByTestId
(
'template-detail-header'
))
.toBeVisible
();
await
expect
(
page
.getByTestId
(
'template-detail-title'
))
.toBeVisible
();
await
expect
(
page
.getByTestId
(
'template-detail-preview'
))
.toBeVisible
();
});
});

在此示例中,测试检查模板标题、标题和预览是否在不显示广泛加载骨架的情况下已可用。如果缺少任何UI元素,测试将失败。

复制链接到标题 优化循环

为了自动化此过程,Next.js 16.4 引入了新的 next-partial-prefetching-optimizer 技能。它通过以下循环引导代理,直到 instant() 测试通过:

code
选择导航和目标UI
│
▼
为当前UI编写通过的 instant() 测试
│
▼
添加目标断言并确认它们失败
│
▼
重构应用 ◀────────────────────────────┐
使用缓存和预取                 │
│                                  │
▼                                  │
重新构建并重新运行测试                    │
│                                  │
├── 仍失败 ─────────────────┘
│
▼ 通过
验证回归检查并保留测试

在较大的代码库中,这尤其有用,因为到达目标UI可能需要多次迭代和重构。代理可以持续运行循环直到 instant() 测试通过。

测试将保留在代码库中,并可在CI中运行以捕获未来的回归问题。

该技能可以从广泛的目标中做出最佳努力。对于这项工作,我们希望对UI加载方式有更多控制,因此下面的提示定义了点击前应准备好的内容以及可以等待的内容。

复制链接到标题 在v0中应用循环

我们要求安装了该技能的代理在v0上运行此工作流程,并将工作拆分为特定功能的PR。对于每个PR,我们在合并前在预览中尝试交互,然后在发布后跟踪四个生产指标:

  • 缓存读写操作的运行时缓存消耗
  • 受影响路由的请求量,包括预取请求和函数调用
  • 用于服务这些路由的活动CPU使用量
  • 预取请求的数据传输量

让我们看看技能如何工作的几个示例。

复制链接到标题 为 /templates/[id] 预取有用内容

重构前,从 /templates 打开卡片时,模板详细信息加载时会显示加载骨架。返回到同一模板时会重新获取这些详细信息并再次显示骨架。

重构前的模板页面。

我们想要的UX是:

提示

code
当我从“模板”页面打开一个模板时,预取其标题、标题和预览,以便立即显示。

为了实现这种UX,代理将公共模板数据与个性化查看器状态分离。公共数据可以在用户之间安全共享,因此代理使用 'use cache: remote' 对其进行缓存。

/think

在 Vercel 上,这会将结果存储在 Runtime Cache 中,不同用户和函数实例的请求可以重复使用它。使用自托管的 next start 时,'use cache: remote' 默认使用内存,除非配置了 cacheHandlers.remote。普通的 'use cache' 也默认使用内存。

缓存数据还需要设置生命周期和失效方式。cacheLife() 用于设置生命周期,而 cacheTag() 提供失效键:

app/.../templates/[id]/page.tsx

code
import
{ cacheLife
,
cacheTag }
from
'next/cache'
;
async
function
loadCachedPublicTemplatePageData
(templateId
:
string
) {
'use cache: remote'
;
cacheTag
(
`template:
${
templateId
}
`
);
cacheLife
(
'hours'
);
return
loadPublicTemplatePageData
(templateId);
}

发布或更新模板时会调用 revalidateTag(),标记缓存数据为过期,这样后续请求会刷新数据。

现在模板数据可以重复使用后,代理在 /templates 的每个链接上添加了 prefetch={true}:

app/templates/template-card.tsx

code
import
Link
from
'next/link'
;
<
Link
href
=
{
`/templates/
${
template
.id
}
`
}
prefetch
=
{
true
}>
<
TemplateCard
template
=
{template} />
</
Link
>;

重构后,模板的标题、预览等内容在点击前已准备就绪,并且用户返回时仍可访问:

重构后的模板页面。

复制链接到标题:避免大型网格中不必要的预取

首次发布使所选模板的内容立即可用,但也导致 /templates 页面中每个可见卡片都触发了更丰富的预取。生产指标显示,/templates/[id] 的预取请求数量增加了 2.9 倍,因此我们调整了更丰富预取的触发时机。

我们仍然希望预取用户可能打开的卡片,因此向代理提出要求:

code
在“Templates”页面上,请等到我悬停、聚焦或触摸卡片后再预取其详细信息。不要在卡片刚出现时就预取所有卡片。

代理将更丰富的预取改为等待用户意图,仅在悬停、聚焦或触摸时触发,而不是卡片进入视口时立即触发。

核心行为可以用几行代码实现:

app/components/prefetch-link.tsx

code
const
[
intent
,
setIntent
]
=
useState
(
false
);
<
Link
href
=
{href}
prefetch
=
{intent
?
true
:
'auto'
}
onMouseEnter
=
{()
=>
setIntent
(
true
)}
onFocus
=
{()
=>
setIntent
(
true
)}
onTouchStart
=
{()
=>
setIntent
(
true
)}
/>;

在 v0 版本中,代理将此行为添加到现有的自定义 Link 包装器中:

code
<
PrefetchLink
href
=
{
`/templates/
${
template
.id
}
`
}
prefetch
=
"intent"
>
<
TemplateCard
template
=
{template} />
</
PrefetchLink
>

对于模板网格,这使得所选模板的详细信息在点击前保持可用,而不会将每个卡片视为同样可能被打开。在整个重构过程中,这种模式也用于 v0 中的其他大型列表和网格。较小的固定分组在链接可见时继续预取。

复制链接到标题:在 /settings 下重用个性化内容

重构前,/settings 下的页面在首次访问时可以显示加载状态,并在用户返回时再次显示。由于内容是个性化的,需要保留在浏览器中,而不是用户之间共享的缓存中。

重构前的设置页面。

我们希望的用户体验是:

code
当我在“设置”页面之间切换时,我希望在点击“高级”、“工作区”和“使用情况”之前,这些页面的内容就已经预先加载。其他页面在打开时再加载,并且如果我返回这些页面,内容应保持可用。

代理使用了 'use cache: private' 来在浏览器中保留这些内容。

/settings 下的 URL 包含在 [scope] 中的账户或团队信息,例如 /acme/settings/advanced。将 scope 传递给缓存函数会将其作为缓存键的一部分,从而确保每个账户或团队的内容相互隔离。高级页面可以重用五分钟后的内容:

app/.../settings/advanced/page.tsx

code
import
{ cacheLife
,
cacheTag }
from
'next/cache'
;
async
function
AdvancedSettings
({ scope }
:
{ scope
:
string
}) {
'use cache: private'
;
cacheLife
({ stale
:
300
});
cacheTag
(
'settings:advanced'
);
return
<
AdvancedSettingsContent
scope
=
{scope} />;
}

对于用户可能修改的缓存数据,代理将每个读取操作与更新它的操作配对。例如,当用户保存 v0 的自定义指令以在聊天和项目中使用时,Server Action 会调用 updateTag()。这会立即使缓存的高级设置失效,从而让用户在同一次请求中看到新值。

一旦内容被缓存,代理在“高级”、“工作区”和“使用情况”的链接中添加了 prefetch={true},以解析每个作用域的目标并将其缓存内容包含在预加载中。/settings 下的其他页面在点击后加载,并在用户返回时保持可用。

重构后,所选页面在打开时内容已准备就绪,而其他页面在首次访问后仍保持可用:

重构后的设置页面。

复制链接到标题 平衡预加载和成本

我们还希望改进从 /projects 进入单个项目时的导航体验。重构前,打开一个项目会显示一个广泛的加载状态。重新打开时会再次获取内容并重新显示加载状态。

重构前的项目页面。

“最近聊天”部分仍然对返回访问有用,但不需要预加载。

Next.js 16.4 包含一个新的 navigation() API,允许您决定哪些缓存工作在导航时等待:

app/.../comments.tsx

code
import
{ navigation }
from
'next/cache'
;
async
function
Comments
() {
await
navigation
();
const
comments
=
await
getComments
();
return
<
CommentList
comments
=
{comments} />;
}

导航后,缓存的工作会在页面打开后等待,然后在用户返回时保持可用。这对于不需要立即访问的低优先级内容效果很好。

对于这种导航,我们希望实现的用户体验是:

code
当我在“项目”页面打开一个项目时,我希望在点击时标题和“生产部署”已经准备就绪。在悬停、聚焦或触摸时预加载它们。“最近聊天”应为返回访问缓存,但不需要预加载。

代理在导航到“最近聊天”之前缓存并预加载了项目详细信息:

app/.../projects/[id]/page.tsx

code
import
{ Suspense }
from
'react'
;
import
{ navigation }
from
'next/cache'
;
function
ProjectPage
({ project }
:
{ project
:
Project
}) {
return
(
<>
<
ProjectDetails
project
=
{project} />
<
Suspense
fallback
=
{<
RecentChatsSkeleton
/>}>
<
RecentChats
projectId
=
{
project
.id} />
</
Suspense
>
</>
);
}
async
function
RecentChats
({ projectId }
:
{ projectId
:
string
}) {
await
navigation
();
const
chats
=
await
loadProjectChats
(projectId);
return
<
ChatList
chats
=
{chats} />;
}
code

另一个使用 navigation() 的示例是 /settings/usage。该代理在预获取阶段保留了标签页、筛选器和 Activity 界面,而信用额度仪表板和使用量字段则在导航过程中加载,并在返回访问时保持可用。

重构后,整体加载状态已消失。项目标题和生产部署部分率先显示,随后是近期聊天记录,该部分会针对返回访问进行缓存:

重构后的项目界面。

## 复制链接到标题 查看结果

我们在每次 PR 后都审查了指标,并根据进展调整了工作。当所有 PR 合并后,全应用对比显示每日总成本降低了 2.9%,即使共享运行时缓存支出增长到了重构前水平的 4.3 倍,每天增加约 3.44 美元。更少的请求数量和更低的计算及传输成本抵消了这一增长,使我们追踪的请求服务成本下降了 14.4%。

在同期,流量和无关部署也发生了变化,因此我们将全应用数据视为确认重构没有使 v0 更加昂贵的依据,而非仅由本次工作带来的节省。

我们还将针对这些导航的 instant() 测试数量翻了一番。在我们修改的区域,用户在打开页面时能立即看到更多有用内容,返回时也遇到更少的加载状态。这些测试现在保护了 v0 演进过程中的体验。

## 复制链接到标题 亲自尝试

你可以使用 next-partial-prefetching-optimizer 技能在你的应用中运行相同的循环。它假设你的应用已经使用了缓存组件和部分预获取功能。缓存组件迁移指南和部分预获取采用指南涵盖了这些前提条件。

为你的编码代理安装技能:

npx skills add vercel/next.js --skill next-partial-prefetching-optimizer

code

安装完成后,大多数编码代理会在请求匹配时自动调用该技能。根据你的工具链,你也可以要求代理调用 next-partial-prefetching-optimizer 技能,或使用 /partial-prefetching-optimizer 命令。

用你自己的话描述想要优化的导航(如上文示例所示),或要求代理在你的应用中寻找优化机会。

若想自行应用这些模式,请遵循《优化预获取指南》。该技能在底层使用相同的指导原则。

## 复制链接到标题 反馈与社区

在你自己的应用中尝试该功能,并告诉我们效果如何:

- GitHub 讨论
- GitHub 问题
- Discord 社区