预取请求涨了2.9倍,账单却降了。Vercel 把这笔账算清楚了。
v0 是 Vercel 的 AI 应用构建器。此前团队分享过一个案例:编码智能体用 Playwright 的 instant() 测试辅助函数,让 v0 里的导航变得即时。那次工作聚焦在核心的 /chat 体验上。
但在 v0 的其他页面,用户回到已经打开过的页面时,看到的还是加载骨架屏。有用的内容也不总能在第一次点击前准备好。团队想让这些页面更像单页应用:进入时预取内容,返回时内容还在。
旧版 Next.js 里,prefetch={true} 会抓取整个目标页面,包括动态内容。这会抓取用户从未打开过的页面;而在预取完成前点击,导航依然很慢。关掉预取能避免这些请求,但把所有工作都推迟到点击之后。
Partial Prefetching(部分预取)换了个思路:Next.js 预取路由中可复用的部分,让它们在点击前就绪,其余部分在导航过程中加载。缓存内容——包括存在浏览器里的个性化内容——可以被预取,并在返回访问时复用。对于选定的链接,prefetch={true} 会解析目标 URL,这样 /products/[id] 这类动态路由的缓存内容也能被纳入预取。
启用方式写在配置里:cacheComponents 和 partialPrefetching 两个开关同时打开。
动手之前,v0 已经在用 Cache Components,/chat 上的早期工作也已经让少数路由逐步用上了 Partial Prefetching。
每次导航都需要一个清晰、可测试的预取 UI 目标。instant() 这个 Playwright 辅助函数像一把锁,卡在点击这个动作上:它拦住只在导航期间才到达的内容,让测试可以断言点击前就已可用的 UI。测试会检查模板头部、标题和预览是否已经可用,且没有显示大面积的加载骨架屏。任何一项缺失,测试就失败。
为了自动化这个过程,Next.js 16.4 带来了新的 next-partial-prefetching-optimizer Skill。它引导智能体循环执行:选择要预取的导航和 UI,为当前 UI 写一个能通过的 instant() 测试,加上目标断言并确认它们失败,调整缓存和预取,重新构建并重跑测试——失败就继续循环,通过就验证回归检查并保留测试。
这在大型代码库里尤其有用:要到达目标 UI 可能需要好几轮迭代和重构,智能体可以一直跑这个循环,直到 instant() 测试通过。测试留在代码库里,可以在 CI 中运行,捕捉未来的回归。
Skill 能从宽泛的目标出发尽力而为。但这次团队想更精确地控制 UI 的加载方式,于是用提示词定义了哪些内容应该在点击前就绪、哪些可以等。
他们让装了 Skill 的智能体在 v0 上跑这套工作流,并把工作拆成按功能划分的 PR。每个 PR 合并前先在预览里试交互,上线后追踪四项生产指标。
重构前,从 /templates 打开一张卡片会显示加载骨架屏,等模板详情加载完;返回同一个模板时,详情会重新抓取,骨架屏又回来了。
想要的效果是:点击前模板头部、标题和预览就绪,返回时它们依然在。
为此,智能体把公开的模板数据和个性化的浏览者状态拆开。公开数据可以跨用户共享,于是用 'use cache: remote' 缓存。在 Vercel 上,结果存进 Runtime Cache,不同用户和函数实例的请求都能复用。自托管 next start 时,'use cache: remote' 默认用内存,除非配置 cacheHandlers.remote;普通的 'use cache' 默认也是内存。
缓存数据还需要生命周期和失效方式。cacheLife() 设定生命周期,cacheTag() 提供失效键。发布或更新模板时调用 revalidateTag(),把缓存数据标记为过期,后续请求再刷新。
模板数据可复用之后,智能体给 /templates 里每个链接加上了 prefetch={true}。
重构后,模板头部、标题和预览在点击前就绪,用户返回时依然可用。
但第一轮上线暴露了问题:它让选中模板的内容立即可用,同时也为 /templates 里每张可见卡片启动了更重的预取。生产指标显示,发往 /templates/[id] 的预取请求涨了 2.9倍。于是团队调整了重预取的启动时机。
他们仍然想预取用户可能打开的那张卡片,于是让智能体把重预取改为等待用户意图:悬停、聚焦或触摸时才开始,而不是卡片进入视口就触发。核心行为几行代码就能表达。对 Templates 网格来说,这既保证了选中模板的详情在点击前可用,又不再把每张卡片都当成同等可能被打开。整个重构过程中,这个模式也用在了 v0 里其他大型列表和网格上;较小的固定分组仍然在链接可见时就预取。
重构前,/settings 下的页面在首次访问和用户返回时都会显示加载状态。因为内容是个性化的,它必须留在浏览器里,而不是放在用户间共享的缓存中。
/settings 下的 URL 在 [scope] 里包含账户或团队,比如 /acme/settings/advanced。把 scope 传进缓存函数,它就进入缓存键,让每个账户或团队的内容彼此隔离。Advanced 页面可以复用结果五分钟。
对于用户可修改的缓存数据,智能体把每次读取和更新它的动作配对。比如用户保存 Custom Instructions 让 v0 在聊天和项目中遵循时,Server Action 调用 updateTag(),立即让缓存的 Advanced 设置过期,用户在同一轮往返里就能看到新值。
这些内容缓存好之后,智能体给 Advanced、Workspace 和 Usage 链接加上 prefetch={true},解析每个带 scope 的目标,把缓存内容纳入预取。/settings 下的其他页面在点击后加载,首次访问后保持可用。
团队还想改善从 /projects 进入单个项目的导航。重构前,打开一个项目会显示大范围加载状态;重新打开会再次抓取内容,加载状态又回来。
Recent Chats 区块对返回访问仍有缓存价值,但不需要预取。Next.js 16.4 新增的 navigation() API 可以决定哪些缓存工作等到导航时再做。navigation() 之后的缓存工作会等到页面打开,然后用户返回时保持可用,适合不急需的低优先级内容。
智能体缓存并预取了项目详情,把 navigation() 放在 Recent Chats 之前。navigation() 的另一个用例是 /settings/usage:标签页、筛选器和 Activity UI 留在预取里,而额度面板和用量字段在导航期间加载,返回访问时保持可用。
重构后,大范围加载状态消失了。项目标题和 Production Deployment 区块先出现,随后是 Recent Chats,它被缓存下来供返回访问。
每个 PR 之后团队都会复盘指标并调整。所有 PR 合并后,全应用对比显示每日总成本降低 2.9%,即便 Shared Runtime Cache 支出涨到重构前的 4.3倍,每天多花约 3.44 美元。更少的请求、更低的计算和传输抵消了这部分增长,把请求服务成本拉低了 14.4%。
同一时期流量和无关部署也在变化,所以团队把全应用数字当作确认——这次重构没有让 v0 变得更贵,而不是把它当成这次工作单独带来的节省。
覆盖这些导航的 instant() 测试数量翻了一倍。在改动的区域里,用户打开页面时看到更多有用内容,返回时更少看到加载状态。这些测试现在保护着这一体验,随着 v0 演进继续生效。
Skill 安装后,大多数编码智能体在请求匹配时会自动调用它。取决于你的运行环境,也可以让智能体调用 next-partial-prefetching-optimizer Skill,或使用 /partial-prefetching-optimizer。用自己的话描述想改善的导航,或者让智能体在你的应用里寻找机会。想自己应用这些模式,可以跟着 Optimizing prefetching 指南走——Skill 底层用的就是同一套指引。
特别声明:以上内容(如有图片或视频亦包括在内)为自媒体平台“网易号”用户上传并发布,本平台仅提供信息存储服务。
Notice: The content above (including the pictures and videos if any) is uploaded and posted by a user of NetEase Hao, which is a social media platform and only provides information storage services.