按导航意图启用 Link 预取
场景
详情页入口出现在长列表中时,Next.js <Link> 的默认视口预取会让一批可见链接提前请求目标路由。对需要读取详情数据或动态生成页面的路由来说,这些请求可能带来并未转化为真实访问的工作。
这里不完全关闭预取,而是把触发时机从“链接进入视口”推迟到“用户表现出导航意图”:鼠标移入,或通过键盘让链接获得焦点。
代码
type HoverPrefetchLinkProps = Omit<ComponentProps<typeof Link>, 'prefetch'>;
export function HoverPrefetchLink({
onMouseEnter,
onFocus,
...props
}: HoverPrefetchLinkProps) {
const [hasNavigationIntent, setHasNavigationIntent] = useState(false);
const enablePrefetch = () => {
setHasNavigationIntent(true);
};
return (
<Link
{...props}
prefetch={hasNavigationIntent ? null : false}
onMouseEnter={(event) => {
enablePrefetch();
onMouseEnter?.(event);
}}
onFocus={(event) => {
enablePrefetch();
onFocus?.(event);
}}
/>
);
}记一下
这个组件的关键不是“悬停时手动调用预取”,而是分两个阶段控制 <Link>:初始的 false 关闭预取,识别到意图后再用 null 恢复 App Router 的默认行为。
null 不是“强制完整预取”。在 App Router 中,它表示自动策略:静态路由通常完整预取,动态路由会根据路由和 loading 边界决定预取范围。这样既保留框架自己的策略,也避免列表刚出现时就同时预取大量详情页。
状态只会从 false 变成 true,不会在 onMouseLeave 时重置。用户一旦对某个链接表现过意图,这个链接在后续渲染中就继续保持可预取,避免鼠标轻微移出后重复切换状态。
onFocus 与 onMouseEnter 同样重要:前者覆盖键盘导航,不能只为鼠标用户优化。当前实现没有为触屏单独增加预取窗口;触屏点击仍然可以正常导航,只是通常会直接进入目标页。
这套语义针对 Next.js App Router。预取只在生产环境启用,因此验证网络请求时应使用生产构建,而不是只看开发模式。
示例直接使用 next/link,方便独立检查。如果项目使用国际化导航等自定义 Link 包装器,只要它继续透传 Next.js Link 的 prefetch 与事件 Props,也可以沿用同样的控制方式。
我做了什么
- 首次渲染传入
prefetch={false},阻止长列表里的链接因为进入视口而批量预取。 - 收到
onMouseEnter或onFocus后记录导航意图。 - 下一次渲染把
prefetch恢复为null,重新启用 App Router 的默认预取策略。 - 继续调用外部传入的同名事件处理函数,不改变包装前的交互能力。
- 从组件 Props 中排除
prefetch,让预取策略只由这个组件管理。
核心流程
链接进入视口
↓
prefetch=false,不发起默认预取
↓
鼠标移入 / 键盘聚焦
↓
hasNavigationIntent=true
↓
重新渲染为 prefetch=null
↓
恢复 Next.js 默认预取策略