跳到内容
App Router入门链接和导航

链接与导航

在 Next.js 中,路由默认在服务器上渲染。这通常意味着客户端必须等待服务器响应才能显示新路由。Next.js 内置了预取流式传输客户端过渡,确保导航快速响应。

本指南解释了 Next.js 中导航的工作原理,以及如何为动态路由慢速网络优化它。

导航工作原理

要了解 Next.js 中导航的工作原理,熟悉以下概念会有所帮助

服务器渲染

在 Next.js 中,布局和页面默认是React 服务器组件。在初始导航和后续导航中,服务器组件负载会在服务器上生成,然后发送到客户端。

根据发生的时间,服务器渲染有两种类型

  • 静态渲染(或预渲染)发生在构建时或重新验证期间,结果会被缓存。
  • 动态渲染在请求时响应客户端请求发生。

服务器渲染的缺点是客户端必须等待服务器响应才能显示新路由。Next.js 通过预取用户可能访问的路由并执行客户端过渡来解决此延迟。

须知:也会为首次访问生成 HTML。

预取

预取是在用户导航到某个路由之前,在后台加载该路由的过程。这使得应用程序中路由之间的导航感觉即时,因为当用户点击链接时,渲染下一个路由所需的数据已经在客户端可用。

Next.js 会在链接进入用户视口时自动预取与<Link> 组件关联的路由。

app/layout.tsx
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>
  )
}

预取路由的多少取决于它是静态的还是动态的

  • 静态路由:预取整个路由。
  • 动态路由:如果存在 loading.tsx,则跳过预取,或部分预取路由。

通过跳过或部分预取动态路由,Next.js 避免了为用户可能永远不会访问的路由在服务器上进行不必要的工作。然而,在导航之前等待服务器响应可能会给用户留下应用程序没有响应的印象。

Server Rendering without Streaming

为了改善动态路由的导航体验,您可以使用流式传输

流式传输

流式传输允许服务器在动态路由的部分准备好后立即将其发送到客户端,而不是等待整个路由渲染完成。这意味着用户可以更快地看到内容,即使页面的一部分仍在加载中。

对于动态路由,这意味着它们可以被部分预取。也就是说,共享布局和加载骨架可以提前请求。

How Server Rendering with Streaming Works

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

loading.js special file
app/dashboard/loading.tsx
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 的优势

  • 即时导航和用户视觉反馈。
  • 共享布局保持交互性,并且导航可中断。
  • 改进核心 Web 指标:TTFBFCPTTI

为了进一步改善导航体验,Next.js 使用 <Link> 组件执行客户端过渡

客户端过渡

传统上,导航到服务器渲染的页面会触发一次完整的页面加载。这会清除状态、重置滚动位置并阻止交互。

Next.js 使用 <Link> 组件通过客户端过渡来避免这种情况。它不是重新加载页面,而是通过以下方式动态更新内容

  • 保留所有共享布局和 UI。
  • 用预取的加载状态或新的页面(如果可用)替换当前页面。

客户端过渡使得服务器渲染的应用程序感觉像客户端渲染的应用程序。当与预取流式传输结合使用时,即使是动态路由也能实现快速过渡。

什么会使过渡变慢?

这些 Next.js 优化使导航快速响应。然而,在某些条件下,过渡仍然可能感觉缓慢。以下是一些常见原因以及如何改善用户体验

没有 loading.tsx 的动态路由

导航到动态路由时,客户端必须等待服务器响应才能显示结果。这可能会给用户留下应用程序没有响应的印象。

我们建议将 loading.tsx 添加到动态路由,以启用部分预取、触发即时导航并在路由渲染时显示加载 UI。

app/blog/[slug]/loading.tsx
export default function Loading() {
  return <LoadingSkeleton />
}

须知:在开发模式下,您可以使用 Next.js Devtools 识别路由是静态还是动态。有关更多信息,请参阅devIndicators

没有 generateStaticParams 的动态片段

如果动态片段本可以被预渲染,但由于缺少generateStaticParams而没有,那么该路由将在请求时回退到动态渲染。

通过添加 `generateStaticParams` 确保路由在构建时静态生成

app/blog/[slug]/page.tsx
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
  // ...
}

慢速网络

在缓慢或不稳定的网络上,预取可能在用户点击链接之前未能完成。这会影响静态和动态路由。在这些情况下,`loading.js` 回退可能不会立即出现,因为它尚未预取。

为了提高感知性能,您可以使用useLinkStatus 钩子在过渡进行中时显示即时反馈。

app/ui/loading-indicator.tsx
'use client'
 
import { useLinkStatus } from 'next/link'
 
export default function LoadingIndicator() {
  const { pending } = useLinkStatus()
  return (
    <span aria-hidden className={`link-hint ${pending ? 'is-pending' : ''}`} />
  )
}

您可以通过添加初始动画延迟(例如 100 毫秒)并将其设置为不可见(例如 opacity: 0)来“防抖”提示。这意味着加载指示器只有在导航时间超过指定延迟时才会显示。有关 CSS 示例,请参阅useLinkStatus 参考

须知:您可以使用其他视觉反馈模式,例如进度条。可在此处查看示例:此处

禁用预取

您可以通过将 <Link> 组件上的 prefetch 属性设置为 false 来选择不进行预取。这在渲染大量链接(例如无限滚动表格)时,有助于避免不必要的资源使用。

<Link prefetch={false} href="/blog">
  Blog
</Link>

然而,禁用预取会带来一些权衡

  • 静态路由只会在用户点击链接时获取。
  • 动态路由需要先在服务器上渲染,然后客户端才能导航到它。

为了在不完全禁用预取的情况下减少资源使用,您可以在悬停时才进行预取。这会将预取限制在用户更可能访问的路由,而不是视口中的所有链接。

app/ui/hover-prefetch-link.tsx
'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>
  )
}

水合未完成

<Link> 是一个客户端组件,它必须在预取路由之前进行水合。在首次访问时,大型 JavaScript 包可能会延迟水合,从而阻止预取立即开始。

React 通过选择性水合来缓解此问题,您可以通过以下方式进一步改进

示例

原生 History API

Next.js 允许您使用原生的 window.history.pushStatewindow.history.replaceState 方法来更新浏览器的历史堆栈,而无需重新加载页面。

pushStatereplaceState 调用集成到 Next.js Router 中,允许您与 usePathnameuseSearchParams 同步。

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>
    </>
  )
}

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>
    </>
  )
}