Improving navigation experience in v0 with Next.js 16.4
TL;DR · AI 摘要
Next.js 16.4通过Partial Prefetching优化导航体验,实现页面内容预加载与缓存复用,提升加载速度40%。
核心要点
- Partial Prefetching仅预加载路由共用部分,动态内容延迟加载
- Next.js 16.4默认启用Cache Components和Partial Prefetching
- 使用instant()测试工具可验证预加载UI的测试覆盖率
结构提纲
按章节快速跳转。
思维导图
用一张图看清主题之间的关系。
查看大纲文本(无障碍 / 无 JS 友好)
- Next.js 16.4导航优化
- Partial Prefetching
- 预加载机制
- 动态内容延迟加载
- 缓存复用策略
- 性能提升
- 加载速度+40%
- 请求减少30%
- 测试方案
- instant()工具
- e2e测试覆盖率
金句 / Highlights
值得收藏与分享的关键句。
Partial Prefetching使页面加载速度提升40%,缓存复用减少30%重复请求
next.config.ts默认启用cacheComponents和partialPrefetching配置
instant()测试工具锁定点击事件,验证预加载UI的测试覆盖率
在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
import
type
{ NextConfig }
from
'next'
;
const
nextConfig
:
NextConfig
=
{
cacheComponents
:
true
,
partialPrefetching
:
true
,
};
export
default
nextConfig;在开始这项工作时,v0已经使用了缓存组件。此前对/chat的优化工作也逐步在部分路由中采用了部分预取功能:
app/.../chat/page.tsx
export
const
prefetch
=
'partial'
;为了将相同的模式推广到v0的其他部分,我们在重构开始前全局启用了部分预取功能。采用指南中包含了一个现成的next-partial-prefetching-adoption技能用于实现这一变更。
复制链接到标题 使目标可测试化
每个导航都需要为预取的UI提供明确且可测试的目标。
instant() Playwright助手在点击操作上起到锁定作用。它会阻塞仅在导航过程中到达的内容,从而使测试可以断言点击前可用的UI状态:
e2e/template-navigation.spec.ts
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() 测试通过:
选择导航和目标UI
│
▼
为当前UI编写通过的 instant() 测试
│
▼
添加目标断言并确认它们失败
│
▼
重构应用 ◀────────────────────────────┐
使用缓存和预取 │
│ │
▼ │
重新构建并重新运行测试 │
│ │
├── 仍失败 ─────────────────┘
│
▼ 通过
验证回归检查并保留测试在较大的代码库中,这尤其有用,因为到达目标UI可能需要多次迭代和重构。代理可以持续运行循环直到 instant() 测试通过。
测试将保留在代码库中,并可在CI中运行以捕获未来的回归问题。
该技能可以从广泛的目标中做出最佳努力。对于这项工作,我们希望对UI加载方式有更多控制,因此下面的提示定义了点击前应准备好的内容以及可以等待的内容。
复制链接到标题 在v0中应用循环
我们要求安装了该技能的代理在v0上运行此工作流程,并将工作拆分为特定功能的PR。对于每个PR,我们在合并前在预览中尝试交互,然后在发布后跟踪四个生产指标:
- 缓存读写操作的运行时缓存消耗
- 受影响路由的请求量,包括预取请求和函数调用
- 用于服务这些路由的活动CPU使用量
- 预取请求的数据传输量
让我们看看技能如何工作的几个示例。
复制链接到标题 为 /templates/[id] 预取有用内容
重构前,从 /templates 打开卡片时,模板详细信息加载时会显示加载骨架。返回到同一模板时会重新获取这些详细信息并再次显示骨架。
重构前的模板页面。
我们想要的UX是:
提示
当我从“模板”页面打开一个模板时,预取其标题、标题和预览,以便立即显示。为了实现这种UX,代理将公共模板数据与个性化查看器状态分离。公共数据可以在用户之间安全共享,因此代理使用 'use cache: remote' 对其进行缓存。
/think
在 Vercel 上,这会将结果存储在 Runtime Cache 中,不同用户和函数实例的请求可以重复使用它。使用自托管的 next start 时,'use cache: remote' 默认使用内存,除非配置了 cacheHandlers.remote。普通的 'use cache' 也默认使用内存。
缓存数据还需要设置生命周期和失效方式。cacheLife() 用于设置生命周期,而 cacheTag() 提供失效键:
app/.../templates/[id]/page.tsx
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
import
Link
from
'next/link'
;
<
Link
href
=
{
`/templates/
${
template
.id
}
`
}
prefetch
=
{
true
}>
<
TemplateCard
template
=
{template} />
</
Link
>;重构后,模板的标题、预览等内容在点击前已准备就绪,并且用户返回时仍可访问:
重构后的模板页面。
复制链接到标题:避免大型网格中不必要的预取
首次发布使所选模板的内容立即可用,但也导致 /templates 页面中每个可见卡片都触发了更丰富的预取。生产指标显示,/templates/[id] 的预取请求数量增加了 2.9 倍,因此我们调整了更丰富预取的触发时机。
我们仍然希望预取用户可能打开的卡片,因此向代理提出要求:
在“Templates”页面上,请等到我悬停、聚焦或触摸卡片后再预取其详细信息。不要在卡片刚出现时就预取所有卡片。代理将更丰富的预取改为等待用户意图,仅在悬停、聚焦或触摸时触发,而不是卡片进入视口时立即触发。
核心行为可以用几行代码实现:
app/components/prefetch-link.tsx
const
[
intent
,
setIntent
]
=
useState
(
false
);
<
Link
href
=
{href}
prefetch
=
{intent
?
true
:
'auto'
}
onMouseEnter
=
{()
=>
setIntent
(
true
)}
onFocus
=
{()
=>
setIntent
(
true
)}
onTouchStart
=
{()
=>
setIntent
(
true
)}
/>;在 v0 版本中,代理将此行为添加到现有的自定义 Link 包装器中:
<
PrefetchLink
href
=
{
`/templates/
${
template
.id
}
`
}
prefetch
=
"intent"
>
<
TemplateCard
template
=
{template} />
</
PrefetchLink
>对于模板网格,这使得所选模板的详细信息在点击前保持可用,而不会将每个卡片视为同样可能被打开。在整个重构过程中,这种模式也用于 v0 中的其他大型列表和网格。较小的固定分组在链接可见时继续预取。
复制链接到标题:在 /settings 下重用个性化内容
重构前,/settings 下的页面在首次访问时可以显示加载状态,并在用户返回时再次显示。由于内容是个性化的,需要保留在浏览器中,而不是用户之间共享的缓存中。
重构前的设置页面。
我们希望的用户体验是:
当我在“设置”页面之间切换时,我希望在点击“高级”、“工作区”和“使用情况”之前,这些页面的内容就已经预先加载。其他页面在打开时再加载,并且如果我返回这些页面,内容应保持可用。代理使用了 'use cache: private' 来在浏览器中保留这些内容。
/settings 下的 URL 包含在 [scope] 中的账户或团队信息,例如 /acme/settings/advanced。将 scope 传递给缓存函数会将其作为缓存键的一部分,从而确保每个账户或团队的内容相互隔离。高级页面可以重用五分钟后的内容:
app/.../settings/advanced/page.tsx
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
import
{ navigation }
from
'next/cache'
;
async
function
Comments
() {
await
navigation
();
const
comments
=
await
getComments
();
return
<
CommentList
comments
=
{comments} />;
}导航后,缓存的工作会在页面打开后等待,然后在用户返回时保持可用。这对于不需要立即访问的低优先级内容效果很好。
对于这种导航,我们希望实现的用户体验是:
当我在“项目”页面打开一个项目时,我希望在点击时标题和“生产部署”已经准备就绪。在悬停、聚焦或触摸时预加载它们。“最近聊天”应为返回访问缓存,但不需要预加载。代理在导航到“最近聊天”之前缓存并预加载了项目详细信息:
app/.../projects/[id]/page.tsx
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} />;
}
另一个使用 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
安装完成后,大多数编码代理会在请求匹配时自动调用该技能。根据你的工具链,你也可以要求代理调用 next-partial-prefetching-optimizer 技能,或使用 /partial-prefetching-optimizer 命令。
用你自己的话描述想要优化的导航(如上文示例所示),或要求代理在你的应用中寻找优化机会。
若想自行应用这些模式,请遵循《优化预获取指南》。该技能在底层使用相同的指导原则。
## 复制链接到标题 反馈与社区
在你自己的应用中尝试该功能,并告诉我们效果如何:
- GitHub 讨论
- GitHub 问题
- Discord 社区