Next.js 中,路由默认在服务器端渲染。这通常意味着客户端在显示新路由之前必须等待服务器响应。Next.js 内置了 预取、流式传输 和 客户端转换 功能,确保导航保持快速和响应。
本指南解释了 Next.js 中的导航工作原理,以及如何为 动态路由 和 慢速网络 优化导航。
要理解 Next.js 中的导航工作原理,了解以下概念会很有帮助:
在 Next.js 中,布局和页面 默认是 React 服务器组件。在首次和后续导航时,服务器组件负载 会在发送到客户端之前在服务器上生成。
服务器渲染有两种类型,基于其发生的 时间:
服务器渲染的权衡是客户端必须等待服务器响应才能显示新路由。Next.js 通过 预取 用户可能访问的路由和执行 客户端转换 来解决此延迟。
须知:HTML 也会在首次访问时生成。
预取是在用户导航到某个路由之前,在后台加载该路由的过程。这使得应用程序中路由之间的导航感觉即时,因为当用户点击链接时,渲染下一个路由所需的数据已经可用于客户端。
Next.js 会在 <Link> 组件 链接的路由进入用户视口时自动预取。
import Link from "next/link";
export default function Layout({ children }: { children: React.ReactNode }) {
return (
<html>
<body>
<nav>
{/* Prefetched when the link is hovered or enters the viewport */}
<Link href="/blog">Blog</Link>
{/* No prefetching */}
<a href="/contact">Contact</a>
</nav>
{children}
</body>
</html>
);
}import Link from "next/link";
export default function Layout() {
return (
<html>
<body>
<nav>
{/* Prefetched when the link is hovered or enters the viewport */}
<Link href="/blog">Blog</Link>
{/* No prefetching */}
<a href="/contact">Contact</a>
</nav>
{children}
</body>
</html>
);
}路由预取的程度取决于它是静态路由还是动态路由:
loading.tsx,则跳过预取,或部分预取路由。通过跳过或部分预取动态路由,Next.js 避免了对用户可能永远不会访问的路由进行不必要的服务器工作。然而,在导航前等待服务器响应可能会让用户觉得应用程序没有响应。

为了改善动态路由的导航体验,您可以使用 流式传输。
流式传输允许服务器在动态路由的各个部分准备好后立即将其发送给客户端,而不是等待整个路由渲染完成。这意味着用户能更快地看到内容,即使页面的一部分仍在加载中。
对于动态路由,这意味着它们可以被部分预取。也就是说,共享布局和加载骨架可以在提前请求。

要使用流式传输,请在路由文件夹中创建 loading.tsx:

export default function Loading() {
// Add fallback UI that will be shown while the route is loading.
return <LoadingSkeleton />;
}export default function Loading() {
// Add fallback UI that will be shown while the route is loading.
return <LoadingSkeleton />;
}在后台,Next.js 会自动将 page.tsx 的内容包装在 <Suspense> 边界中。预取的后备 UI 将在路由加载时显示,并在内容准备好后替换为实际内容。
须知:您还可以使用
<Suspense>为嵌套组件创建加载 UI。
loading.tsx 的好处:
为了进一步改善导航体验,Next.js 使用 <Link> 组件执行 客户端转换。
传统上,导航到服务器渲染的页面会触发一次完整的页面加载。这会清除状态、重置滚动位置并阻塞交互性。
Next.js 通过使用 <Link> 组件的客户端转换来避免这种情况。它不是重新加载页面,而是通过以下方式动态更新内容:
客户端转换是使服务器渲染的应用程序 感觉 像客户端渲染应用程序的原因。当与 预取 和 流式传输 结合使用时,它甚至可以为动态路由实现快速转换。
这些 Next.js 优化使导航快速且响应。然而,在某些条件下,转换仍然会 感觉 缓慢。以下是一些常见原因以及如何改善用户体验:
loading.tsx 的动态路由导航到动态路由时,客户端必须等待服务器响应才能显示结果。这可能会让用户觉得应用程序没有响应。
我们建议将 loading.tsx 添加到动态路由,以启用部分预取、触发即时导航并在路由渲染时显示加载 UI。
export default function Loading() {
return <LoadingSkeleton />;
}export default function Loading() {
return <LoadingSkeleton />;
}须知:在开发模式下,您可以使用 Next.js Devtools 来识别路由是静态还是动态。有关更多信息,请参阅
devIndicators。
generateStaticParams 的动态分段如果一个 动态分段 可以被预渲染但因为缺少 generateStaticParams 而未被预渲染,那么该路由将在请求时回退到动态渲染。
通过添加 generateStaticParams 确保路由在构建时静态生成:
export async function generateStaticParams() {
const posts = await fetch("https://.../posts").then((res) => res.json());
return posts.map((post) => ({
slug: post.slug,
}));
}
export default async function Page({
params,
}: {
params: Promise<{ slug: string }>;
}) {
const { slug } = await params;
// ...
}export async function generateStaticParams() {
const posts = await fetch('https://.../posts').then((res) => res.json())
return posts.map((post) => ({
slug: post.slug,
}))
export default async function Page({ params }) {
const { slug } = await params
// ...
}在慢速或不稳定的网络上,预取可能在用户点击链接之前未能完成。这会影响静态和动态路由。在这些情况下,loading.js 后备项可能不会立即出现,因为它尚未被预取。
为了提高感知性能,您可以使用 useLinkStatus Hook 在转换进行时显示即时反馈。
"use client";
import { useLinkStatus } from "next/link";
export default function LoadingIndicator() {
const { pending } = useLinkStatus();
return (
<span
aria-hidden
className={`link-hint ${pending ? "is-pending" : ""}`}
/>
);
}"use client";
import { useLinkStatus } from "next/link";
export default function LoadingIndicator() {
const { pending } = useLinkStatus();
return (
<span
aria-hidden
className={`link-hint ${pending ? "is-pending" : ""}`}
/>
);
}您可以通过添加初始动画延迟(例如 100ms)并将其设置为不可见(例如 opacity: 0)来“防抖”提示。这意味着加载指示器只会在导航时间超过指定延迟时显示。有关 CSS 示例,请参阅 useLinkStatus 参考。
须知:您可以使用其他视觉反馈模式,例如进度条。 在此处 查看示例。
您可以通过在 <Link> 组件上将 prefetch 属性设置为 false 来选择禁用预取。当渲染大量链接(例如无限滚动表格)时,这有助于避免不必要的资源使用。
<Link prefetch={false} href="/blog">
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>
);
}"use client";
import Link from "next/link";
import { useState } from "react";
function HoverPrefetchLink({ href, children }) {
const [active, setActive] = useState(false);
return (
<Link
href={href}
prefetch={active ? null : false}
onMouseEnter={() => setActive(true)}
>
{children}
</Link>
);
}<Link> 是一个客户端组件,必须在它能够预取路由之前完成水合作用。在首次访问时,大型 JavaScript 包可能会延迟水合作用,从而阻止预取立即开始。
React 通过选择性水合作用 (Selective Hydration) 缓解了这个问题,您可以通过以下方式进一步改进:
@next/bundle-analyzer 插件通过移除大型依赖项来识别和减少包大小。Next.js 允许您使用原生的 window.history.pushState 和 window.history.replaceState 方法来更新浏览器的历史堆栈,而无需重新加载页面。
pushState 和 replaceState 调用集成到 Next.js 路由器中,允许您与 usePathname 和 useSearchParams 同步。
window.history.pushState使用它向浏览器的历史堆栈添加一个新条目。用户可以导航回之前的状态。例如,对产品列表进行排序:
"use client";
import { useSearchParams } from "next/navigation";
export default function SortProducts() {
const searchParams = useSearchParams();
function updateSorting(sortOrder: string) {
const params = new URLSearchParams(searchParams.toString());
params.set("sort", sortOrder);
window.history.pushState(null, "", `?${params.toString()}`);
}
return (
<>
<button onClick={() => updateSorting("asc")}>Sort Ascending</button>
<button onClick={() => updateSorting("desc")}>
Sort Descending
</button>
</>
);
}"use client";
import { useSearchParams } from "next/navigation";
export default function SortProducts() {
const searchParams = useSearchParams();
function updateSorting(sortOrder) {
const params = new URLSearchParams(searchParams.toString());
params.set("sort", sortOrder);
window.history.pushState(null, "", `?${params.toString()}`);
}
return (
<>
<button onClick={() => updateSorting("asc")}>Sort Ascending</button>
<button onClick={() => updateSorting("desc")}>
Sort Descending
</button>
</>
);
}window.history.replaceState使用它来替换浏览器历史堆栈中的当前条目。用户无法导航回之前的状态。例如,切换应用程序的语言环境:
"use client";
import { usePathname } from "next/navigation";
export function LocaleSwitcher() {
const pathname = usePathname();
function switchLocale(locale: string) {
// e.g. '/en/about' or '/fr/contact'
const newPath = `/${locale}${pathname}`;
window.history.replaceState(null, "", newPath);
}
return (
<>
<button onClick={() => switchLocale("en")}>English</button>
<button onClick={() => switchLocale("fr")}>French</button>
</>
);
}"use client";
import { usePathname } from "next/navigation";
export function LocaleSwitcher() {
const pathname = usePathname();
function switchLocale(locale) {
// e.g. '/en/about' or '/fr/contact'
const newPath = `/${locale}${pathname}`;
window.history.replaceState(null, "", newPath);
}
return (
<>
<button onClick={() => switchLocale("en")}>English</button>
<button onClick={() => switchLocale("fr")}>French</button>
</>
);
}