跳到內容
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>
    </>
  )
}