链接和导航
¥Linking and Navigating
在 Next.js 中,路由默认在服务器上渲染。这通常意味着客户端必须等待服务器响应才能显示新的路由。Next.js 内置 prefetching、streaming 和 客户端转换,确保导航保持快速响应。
¥In Next.js, routes are rendered on the server by default. This often means the client has to wait for a server response before a new route can be shown. Next.js comes with built-in prefetching, streaming, and client-side transitions ensuring navigation stays fast and responsive.
本指南讲解了 Next.js 中的导航工作原理,以及如何针对 动态路由 和 网络速度慢 进行优化。
¥This guide explains how navigation works in Next.js and how you can optimize it for dynamic routes and slow networks.
导航的工作原理
¥How navigation works
为了理解 Next.js 中导航的工作原理,熟悉以下概念会有所帮助:
¥To understand how navigation works in Next.js, it helps to be familiar with the following concepts:
服务器渲染
¥Server Rendering
在 Next.js 中,布局和页面 默认为 React 服务器组件。在初始和后续导航中,服务器组件负载 会在服务器上生成,然后再发送到客户端。
¥In Next.js, Layouts and Pages are React Server Components by default. On initial and subsequent navigations, the Server Component Payload is generated on the server before being sent to the client.
根据渲染发生的时间,服务器渲染有两种类型:
¥There are two types of server rendering, based on when it happens:
静态渲染(或预渲染)发生在构建时或 revalidation 期间,结果会被缓存。
¥Static Rendering (or Prerendering) happens at build time or during revalidation and the result is cached.
动态渲染在请求时发生,以响应客户端请求。
¥Dynamic Rendering happens at request time in response to a client request.
服务器渲染的缺点是客户端必须等待服务器响应才能显示新路由。Next.js 通过查找用户可能访问的 prefetching 路由并执行 客户端转换 来解决此延迟问题。
¥The trade-off of server rendering is that the client must wait for the server to respond before the new route can be shown. Next.js addresses this delay by prefetching routes the user is likely to visit and performing client-side transitions.
需要了解:初次访问也会生成 HTML。
¥Good to know: HTML is also generated for the initial visit.
预请求
¥Prefetching
预取是指在用户导航到路由之前在后台加载路由的过程。这使得应用中的路由之间导航感觉很即时,因为当用户点击链接时,渲染下一个路由的数据已经在客户端可用。
¥Prefetching is the process of loading a route in the background before the user navigates to it. This makes navigation between routes in your application feel instant, because by the time a user clicks on a link, the data to render the next route is already available client side.
Next.js 会在与 <Link>
组件 关联的路由进入用户视口时自动预取这些路由。
¥Next.js automatically prefetches routes linked with the <Link>
component when they enter the user's viewport.
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>
)
}
路由的预取量取决于它是静态的还是动态的:
¥How much of the route is prefetched depends on whether it's static or dynamic:
静态路由:预取完整路由。
¥Static Route: the full route is prefetched.
动态路由:跳过预取,或者如果存在
loading.tsx
,则部分预取路由。¥Dynamic Route: prefetching is skipped, or the route is partially prefetched if
loading.tsx
is present.
通过跳过或部分预取动态路由,Next.js 避免了服务器端对用户可能永远不会访问的路由进行不必要的工作。但是,在导航之前等待服务器响应会给用户留下应用没有响应的印象。
¥By skipping or partially prefetching dynamic routes, Next.js avoids unnecessary work on the server for routes the users may never visit. However, waiting for a server response before navigation can give the users the impression that the app is not responding.

为了提升动态路由的导航体验,你可以使用 streaming。
¥To improve the navigation experience to dynamic routes, you can use streaming.
流式
¥Streaming
流式元数据允许服务器在动态路由的部分内容准备就绪后立即将其发送到客户端,而不是等待整个路由渲染完成。这意味着即使页面的某些部分仍在加载,用户也能更快地看到内容。
¥Streaming allows the server to send parts of a dynamic route to the client as soon as they're ready, rather than waiting for the entire route to be rendered. This means users see something sooner, even if parts of the page are still loading.
对于动态路由,这意味着它们可以部分预取。也就是说,可以提前请求共享布局和加载框架。
¥For dynamic routes, it means they can be partially prefetched. That is, shared layouts and loading skeletons can be requested ahead of time.

要使用流式传输,请在路由文件夹中创建一个 loading.tsx
:
¥To use streaming, create a loading.tsx
in your route folder:

export default function Loading() {
// Add fallback UI that will be shown while the route is loading.
return <LoadingSkeleton />
}
在后台,Next.js 会自动将 page.tsx
内容封装在 <Suspense>
边界中。预取的回退 UI 将在路由加载时显示,并在加载完成后替换为实际内容。
¥Behind the scenes, Next.js will automatically wrap the page.tsx
contents in a <Suspense>
boundary. The prefetched fallback UI will be shown while the route is loading, and swapped for the actual content once ready.
需要了解:你还可以使用
<Suspense>
为嵌套组件创建加载 UI。¥Good to know: You can also use
<Suspense>
to create loading UI for nested components.
loading.tsx
的优点:
¥Benefits of loading.tsx
:
为用户提供即时导航和视觉反馈。
¥Immediate navigation and visual feedback for the user.
共享布局保持交互性,导航可中断。
¥Shared layouts remain interactive and navigation is interruptible.
为了进一步提升导航体验,Next.js 使用 <Link>
组件执行 客户端转换。
¥To further improve the navigation experience, Next.js performs a client-side transition with the <Link>
component.
客户端转换
¥Client-side transitions
传统上,导航到服务器渲染的页面会触发完整的页面加载。这会清除状态、重置滚动位置并阻止交互。
¥Traditionally, navigation to a server-rendered page triggers a full page load. This clears state, resets scroll position, and blocks interactivity.
Next.js 通过使用 <Link>
组件进行客户端转换来避免这种情况。它不是重新加载页面,而是通过以下方式动态更新内容:
¥Next.js avoids this with client-side transitions using the <Link>
component. Instead of reloading the page, it updates the content dynamically by:
保留所有共享布局和 UI。
¥Keeping any shared layouts and UI.
将当前页面替换为预取的加载状态或新页面(如果可用)。
¥Replacing the current page with the prefetched loading state or a new page if available.
客户端转换使服务器渲染的应用感觉像客户端渲染的应用。当与 prefetching 和 streaming 配合使用时,即使对于动态路由,它也能实现快速转换。
¥Client-side transitions are what makes a server-rendered apps feel like client-rendered apps. And when paired with prefetching and streaming, it enables fast transitions, even for dynamic routes.
哪些因素会导致转换速度变慢?
¥What can make transitions slow?
这些 Next.js 优化使导航快速且响应迅速。但是,在某些情况下,转换仍然会感觉很慢。以下是一些常见原因以及如何提升用户体验:
¥These Next.js optimizations make navigation fast and responsive. However, under certain conditions, transitions can still feel slow. Here are some common causes and how to improve the user experience:
不带 loading.tsx
的动态路由
¥Dynamic routes without loading.tsx
导航到动态路由时,客户端必须等待服务器响应才能显示结果。这会给用户一种应用没有响应的印象。
¥When navigating to a dynamic route, the client must wait for the server response before showing the result. This can give the users the impression that the app is not responding.
我们建议将 loading.tsx
添加到动态路由中,以启用部分预取、触发即时导航并在路由渲染时显示加载 UI。
¥We recommend adding loading.tsx
to dynamic routes to enable partial prefetching, trigger immediate navigation, and display a loading UI while the route renders.
export default function Loading() {
return <LoadingSkeleton />
}
需要了解:在开发模式下,你可以使用 Next.js Devtools 来识别路由是静态路由还是动态路由。请参阅
devIndicators
了解更多信息。¥Good to know: In development mode, you can use the Next.js Devtools to identify if the route is static or dynamic. See
devIndicators
for more information.
不带 generateStaticParams
的动态片段
¥Dynamic segments without generateStaticParams
如果 动态段 可以预渲染,但由于缺少 generateStaticParams
而无法预渲染,则路由将在请求时回退到动态渲染。
¥If a dynamic segment could be prerendered but isn't because it's missing generateStaticParams
, the route will fallback to dynamic rendering at request time.
通过添加 generateStaticParams
确保路由在构建时静态生成:
¥Ensure the route is statically generated at build time by adding 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
// ...
}
网络速度慢
¥Slow networks
在速度慢或不稳定的网络中,预加载可能无法在用户点击链接之前完成。这会影响静态和动态路由。在这种情况下,loading.js
回退可能不会立即显示,因为它尚未预加载。
¥On slow or unstable networks, prefetching may not finish before the user clicks a link. This can affect both static and dynamic routes. In these cases, the loading.js
fallback may not appear immediately because it hasn't been prefetched yet.
为了提升感知性能,你可以使用 useLinkStatus
钩 在过渡过程中向用户显示内联视觉反馈(例如旋转图标或链接上的文本闪烁)。
¥To improve perceived performance, you can use the useLinkStatus
hook to show inline visual feedback to the user (like spinners or text glimmers on the link) while a transition is in progress.
'use client'
import { useLinkStatus } from 'next/link'
export default function LoadingIndicator() {
const { pending } = useLinkStatus()
return pending ? (
<div role="status" aria-label="Loading" className="spinner" />
) : null
}
你可以通过添加初始动画延迟(例如 100 毫秒)并将动画设置为不可见(例如 opacity: 0
)来 "debounce" 加载指示器。这意味着只有当导航时间超过指定的延迟时,才会显示加载指示器。
¥You can "debounce" the loading indicator by adding an initial animation delay (e.g. 100ms) and starting the animation as invisible (e.g. opacity: 0
). This means the loading indicator will only be shown if the navigation takes longer than the specified delay.
.spinner {
/* ... */
opacity: 0;
animation:
fadeIn 500ms 100ms forwards,
rotate 1s linear infinite;
}
@keyframes fadeIn {
from {
opacity: 0;
}
to {
opacity: 1;
}
}
@keyframes rotate {
to {
transform: rotate(360deg);
}
}
需要了解:你可以使用其他视觉反馈模式,例如进度条。查看示例 此处。
¥Good to know: You can use other visual feedback patterns like a progress bar. View an example here.
禁用预取
¥Disabling prefetching
你可以通过在 <Link>
组件上将 prefetch
属性设置为 false
来选择退出预取。这在渲染大量链接列表(例如无限滚动表)时有助于避免不必要的资源使用。
¥You can opt out of prefetching by setting the prefetch
prop to false
on the <Link>
component. This is useful to avoid unnecessary usage of resources when rendering large lists of links (e.g. an infinite scroll table).
<Link prefetch={false} href="/blog">
Blog
</Link>
但是,禁用预取需要权衡利弊:
¥However, disabling prefetching comes with trade-offs:
静态路由仅在用户点击链接时才会被获取。
¥Static routes will only be fetched when the user clicks the link.
动态路由需要先在服务器上渲染,然后客户端才能导航到该服务器。
¥Dynamic routes will need to be rendered on the server first before the client can navigate to it.
为减少资源使用量而不完全禁用预取功能,你可以仅在鼠标悬停时进行预取。这将预取限制在用户更有可能访问的路由上,而不是视口中的所有链接。
¥To reduce resource usage without fully disabling prefetch, you can prefetch only on hover. This limits prefetching to routes the user is more likely to visit, rather than all links in the viewport.
'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>
)
}
Hydration 未完成
¥Hydration not completed
<Link>
是一个客户端组件,必须先进行数据预取 (hydrated) 才能预取路由。在初始访问时,大型 JavaScript 包可能会延迟数据同步,从而阻止预加载立即开始。
¥<Link>
is a Client Component and must be hydrated before it can prefetch routes. On the initial visit, large JavaScript bundles can delay hydration, preventing prefetching from starting right away.
React 通过选择性水合 (Selective Hydration) 缓解了这个问题,你可以通过以下方式进一步改进:
¥React mitigates this with Selective Hydration and you can further improve this by:
使用
@next/bundle-analyzer
插件识别并通过移除大型依赖来减少包大小。¥Using the
@next/bundle-analyzer
plugin to identify and reduce bundle size by removing large dependencies.尽可能将逻辑从客户端移至服务器。有关指导,请参阅 服务器和客户端组件 文档。
¥Moving logic from the client to the server where possible. See the Server and Client Components docs for guidance.
示例
¥Examples
原生 History API
¥Native History API
Next.js 允许你使用原生 window.history.pushState
和 window.history.replaceState
方法来更新浏览器的历史堆栈,而无需重新加载页面。
¥Next.js allows you to use the native window.history.pushState
and window.history.replaceState
methods to update the browser's history stack without reloading the page.
pushState
和 replaceState
调用集成到 Next.js 路由中,允许你与 usePathname
和 useSearchParams
同步。
¥pushState
and replaceState
calls integrate into the Next.js Router, allowing you to sync with usePathname
and useSearchParams
.
window.history.pushState
使用它可以将新条目添加到浏览器的历史记录堆栈中。用户可以导航回之前的状态。例如,要对产品列表进行排序:
¥Use it to add a new entry to the browser's history stack. The user can navigate back to the previous state. For example, to sort a list of products:
'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>
</>
)
}
window.history.replaceState
用它来替换浏览器历史记录堆栈上的当前条目。用户无法导航回之前的状态。例如,要切换应用的区域设置:
¥Use it to replace the current entry on the browser's history stack. The user is not able to navigate back to the previous state. For example, to switch the application's locale:
'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>
</>
)
}