跳到主要内容

预取

预取使应用程序中不同路由之间的导航感觉瞬间完成。Next.js 默认会基于应用程序代码中使用的链接智能地进行预取。

本指南将解释预取的工作原理并展示常见的实现模式:

预取是如何工作的?

在路由之间导航时,浏览器会请求页面的资源,如 HTML 和 JavaScript 文件。预取是在您导航到新路由之前提前获取这些资源的过程。

Next.js 自动将您的应用程序基于路由分割成较小的 JavaScript 块。与像传统 SPA 那样预先加载所有代码不同,只加载当前路由所需的代码。这减少了初始加载时间,而应用程序的其他部分在后台加载。当您点击链接时,新路由的资源已经加载到浏览器缓存中。

当导航到新页面时,没有完整的页面重新加载或浏览器加载旋转器。相反,Next.js 执行客户端转换,使页面导航感觉瞬间完成。

静态路由与动态路由的预取

静态页面动态页面
预取是,完整路由否,除非有 loading.js
客户端缓存 TTL5 分钟(默认)关闭,除非启用
点击时的服务器往返是,在外壳之后流式传输

提示: 在初始导航期间,浏览器获取 HTML、JavaScript 和 React 服务器组件(RSC)有效负载。对于后续导航,浏览器将获取服务器组件的 RSC 有效负载和客户端组件的 JS 包。

自动预取

app/ui/nav-link.tsx
import Link from 'next/link'

export default function NavLink() {
return <Link href="/about">关于</Link>
}
上下文预取的有效负载客户端缓存 TTL
没有 loading.js整个页面直到应用重新加载
loading.js布局到第一个加载边界30 秒(可配置

自动预取仅在生产环境中运行。使用 prefetch={false} 禁用或使用禁用预取中的包装器。

手动预取

'use client'

import { useRouter } from 'next/navigation'

const router = useRouter()
router.prefetch('/pricing')

调用 router.prefetch() 来预热视口外的路由或响应分析、悬停、滚动等。

悬停触发预取

谨慎进行: 扩展 Link 会让您选择维护预取、缓存失效和可访问性问题。只有在默认值不足时才进行。

Next.js 默认尝试做正确的预取,但高级用户可以根据需要弹出和修改。您在性能和资源消耗之间有控制权。

例如,您可能必须只在悬停时触发预取,而不是在进入视口时(默认行为):

'use client'

import Link from 'next/link'
import { useState } from 'react'

export function HoverPrefetchLink({
href,
children,
}: {
href: string
children: React.ReactNode
}) {
const [active, setActive] = useState(false)

return (
<Link
href={href}
prefetch={active ? null : false}
onMouseEnter={() => setActive(true)}
>
{children}
</Link>
)
}

prefetch={null} 在用户显示意图后恢复默认(静态)预取。

扩展或弹出链接

您可以扩展 <Link> 组件来创建自己的自定义预取策略。例如,使用 ForesightJS 库,它通过预测用户光标的方向来预取链接。

或者,您可以使用 useRouter 来重新创建一些原生 <Link> 行为。但是,请注意这会让您选择维护预取和缓存失效。

'use client'

import { useRouter } from 'next/navigation'
import { useEffect } from 'react'

function ManualPrefetchLink({
href,
children,
}: {
href: string
children: React.ReactNode
}) {
const router = useRouter()

useEffect(() => {
let cancelled = false
const poll = () => {
if (!cancelled) router.prefetch(href, { onInvalidate: poll })
}
poll()
return () => {
cancelled = true
}
}, [href, router])

return (
<a
href={href}
onClick={(event) => {
event.preventDefault()
router.push(href)
}}
>
{children}
</a>
)
}

当 Next.js 怀疑缓存数据过时时,会调用 onInvalidate,允许您刷新预取。

提示: 使用 a 标签将导致到目标路由的完整页面导航,您可以使用 onClick 来防止完整页面导航,然后调用 router.push 导航到目标。

禁用预取

您可以完全禁用某些路由的预取,以便对资源消耗进行更精细的控制。

'use client'

import Link, { LinkProps } from 'next/link'

function NoPrefetchLink({
prefetch,
...rest
}: LinkProps & { children: React.ReactNode }) {
return <Link {...rest} prefetch={false} />
}

例如,您可能仍然希望在应用程序中一致地使用 <Link>,但页脚中的链接在进入视口时可能不需要预取。

预取优化

提示: 布局去重和预取调度是即将推出的优化的一部分。目前可通过 experimental.clientSegmentCache 标志在 Next.js canary 中使用。

客户端缓存

Next.js 将预取的 React 服务器组件有效负载存储在内存中,按路由段键控。当在同级路由之间导航时(例如 /dashboard/settings/dashboard/analytics),它重用父布局并只获取更新的叶子页面。这减少了网络流量并提高了导航速度。

预取调度

Next.js 维护一个小任务队列,按以下顺序进行预取:

  1. 视口中的链接
  2. 显示用户意图的链接(悬停或触摸)
  3. 较新的链接替换较旧的链接
  4. 滚动离屏的链接被丢弃

调度器优先考虑可能的导航,同时最小化未使用的下载。

部分预渲染(PPR)

启用 PPR 时,页面分为静态外壳和流式动态部分:

  • 可以预取的外壳立即流式传输
  • 动态数据在准备好时流式传输
  • 数据失效(revalidateTagrevalidatePath)静默刷新相关的预取

故障排除

在预取期间触发不想要的副作用

如果您的布局或页面不是纯的并且有副作用(例如跟踪分析),这些可能在路由被预取时触发,而不是在用户访问页面时。

为了避免这种情况,您应该将副作用移动到 useEffect 钩子或从客户端组件触发的服务器操作中。

之前

app/dashboard/layout.tsx
import { trackPageView } from '@/lib/analytics'

export default function Layout({ children }: { children: React.ReactNode }) {
// 这在预取期间运行
trackPageView()

return <div>{children}</div>
}

之后

app/ui/analytics-tracker.tsx
'use client'

import { useEffect } from 'react'
import { trackPageView } from '@/lib/analytics'

export function AnalyticsTracker() {
useEffect(() => {
trackPageView()
}, [])

return null
}
app/dashboard/layout.tsx
import { AnalyticsTracker } from '@/app/ui/analytics-tracker'

export default function Layout({ children }: { children: React.ReactNode }) {
return (
<div>
<AnalyticsTracker />
{children}
</div>
)
}

防止过多的预取

Next.js 在使用 <Link> 组件时自动预取视口中的链接。

在某些情况下,您可能希望防止这种情况以避免不必要的资源使用,例如在渲染大量链接列表时(例如无限滚动表格)。

您可以通过将 <Link> 组件的 prefetch 属性设置为 false 来禁用预取。

app/ui/no-prefetch-link.tsx
<Link prefetch={false} href={`/blog/${post.id}`}>
{post.title}
</Link>

但是,这意味着静态路由只会在点击时获取,动态路由将等待服务器渲染后再导航。

为了减少资源使用而不完全禁用预取,您可以将预取推迟到用户悬停在链接上。这只针对用户可能访问的链接。

app/ui/hover-prefetch-link.tsx
'use client'

import Link from 'next/link'
import { useState } from 'react'

export function HoverPrefetchLink({
href,
children,
}: {
href: string
children: React.ReactNode
}) {
const [active, setActive] = useState(false)

return (
<Link
href={href}
prefetch={active ? null : false}
onMouseEnter={() => setActive(true)}
>
{children}
</Link>
)
}