链接与导航
Next.js 的路由默认在服务端渲染,为了让用户在路由切换时也感觉流畅,框架内置了 预取、流式传输 和 客户端过渡 三大优化。本章讲透它们是如何协作的,以及怎样为慢网络和动态路由做优化。
导航的四层原理
- Server Rendering:布局和页面默认是 RSC(React Server Components),服务端生成 RSC 载荷后发给客户端。
- Prefetching:当链接进入视口时,Next.js 自动在后台预取目标路由。
- Streaming:允许服务端边渲染边发送,配合
loading.tsx立即显示加载骨架。 - Client-side Transitions:
<Link>使用客户端过渡替换页面内容,保留共享布局、滚动状态与交互。
用 <Link> 替代 <a>
import Link from 'next/link';
export default function Layout({ children }: { children: React.ReactNode }) {
return (
<html>
<body>
<nav>
<Link href="/blog">博客</Link> {/* 自动预取 */}
<a href="/contact">联系我们</a> {/* 不会预取 */}
</nav>
{children}
</body>
</html>
);
}
- 静态路由:整条路由都会被预取。
- 动态路由:默认不预取;若存在
loading.tsx,则会部分预取(共享布局 + 骨架)。
用 loading.tsx 开启流式渲染
在路由文件夹里放 loading.tsx,Next.js 会自动把 page.tsx 包在 <Suspense> 里:
// app/blog/loading.tsx
export default function Loading() {
return <BlogListSkeleton />;
}
效果:用户点击链接后立即看到骨架屏,真实内容准备好再替换进来。对 Core Web Vitals(TTFB / FCP / TTI)都有明显提升。
动态段建议加 generateStaticParams
若动态段本可以在构建期预渲染,却没有 generateStaticParams,每次访问都会退化为请求时渲染:
export async function generateStaticParams() {
const posts = await fetch('https://.../posts').then(r => r.json());
return posts.map((p: any) => ({ slug: p.slug }));
}
慢网络下的体验优化
网络不稳时预取可能没跑完,骨架也来不及显示。用 useLinkStatus 立刻给用户一个视觉反馈:
'use client';
import { useLinkStatus } from 'next/link';
export default function LinkIndicator() {
const { pending } = useLinkStatus();
return <span aria-hidden className={pending ? 'is-pending' : ''} />;
}
结合 CSS 延迟动画(比如 100ms 才显示)可以避免快速导航时的「闪屏」。
按需关闭预取
长列表(无限滚动表格等)预取会很浪费,可以直接关掉:
<Link prefetch={false} href="/blog">博客</Link>
或者只在鼠标悬停时预取,兼顾资源与体验:
'use client';
import Link from 'next/link';
import { useState } from 'react';
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>
);
}
使用原生 History API
不想触发 UI 重新渲染但又想同步 URL,可以直接用 pushState:
'use client';
import { useSearchParams } from 'next/navigation';
export default function SortProducts() {
const searchParams = useSearchParams();
function update(order: string) {
const params = new URLSearchParams(searchParams.toString());
params.set('sort', order);
window.history.pushState(null, '', `?${params.toString()}`);
}
return (
<>
<button onClick={() => update('asc')}>升序</button>
<button onClick={() => update('desc')}>降序</button>
</>
);
}
pushState / replaceState 都会与 usePathname、useSearchParams 同步。