Menu

リンクとナビゲーション

Next.jsでは、ルートはデフォルトでサーバー側でレンダリングされます。つまり、新しいルートを表示する前にクライアントがサーバーレスポンスを待つ必要があることが多いです。Next.jsには、ナビゲーションを高速かつレスポンシブに保つための組み込みのプリフェッチストリーミングクライアント側トランジションが備わっています。

このガイドでは、Next.jsでナビゲーションがどのように機能するか、また動的ルート低速ネットワークのナビゲーション最適化方法について説明します。

ナビゲーションの仕組み

Next.jsでのナビゲーション方法を理解するには、以下の概念に精通していると役立ちます。

サーバーレンダリング

Next.jsでは、レイアウトとページはデフォルトでReact Server Componentsです。初回および以降のナビゲーション時に、Server Component Payloadがサーバー側で生成され、クライアントに送信されます。

サーバーレンダリングには、_いつ実行されるか_に基づいて2つのタイプがあります。

  • 静的レンダリング(またはプリレンダリング) はビルド時または再検証中に実行され、その結果はキャッシュされます。
  • 動的レンダリング はクライアントリクエストに応じてリクエスト時に実行されます。

サーバーレンダリングのトレードオフは、新しいルートを表示する前にクライアントがサーバーレスポンスを待つ必要があることです。Next.jsはユーザーが訪問する可能性の高いルートをプリフェッチし、クライアント側トランジションを実行することでこの遅延に対処しています。

補足:初回訪問時にもHTMLが生成されます。

プリフェッチ

プリフェッチはユーザーがナビゲーションする前にバックグラウンドでルートを読み込むプロセスです。ユーザーがリンクをクリックする時点で次のルートをレンダリングするデータがクライアント側で既に利用可能なため、アプリケーション内のルート間のナビゲーションはインスタントに感じられます。

Next.jsは<Link>コンポーネントでリンクされたルートをユーザーのビューポートに入ると自動的にプリフェッチします。

app/layout.tsx
TypeScript
import Link from 'next/link'
 
export default function Layout({ children }: { children: React.ReactNode }) {
  return (
    <html>
      <body>
        <nav>
          {/* リンクがホバーされるか、ビューポートに入るとプリフェッチされます */}
          <Link href="/blog">Blog</Link>
          {/* プリフェッチなし */}
          <a href="/contact">Contact</a>
        </nav>
        {children}
      </body>
    </html>
  )
}

ルートのどの程度がプリフェッチされるかは、それが静的か動的かによります。

  • 静的ルート:ルート全体がプリフェッチされます。
  • 動的ルート:プリフェッチはスキップされるか、loading.tsxが存在する場合はルートは部分的にプリフェッチされます。

動的ルートをスキップまたは部分的にプリフェッチすることで、Next.jsはユーザーが訪問しない可能性のあるルートのサーバー上での不要な処理を回避します。ただし、ナビゲーション前にサーバーレスポンスを待つことにより、ユーザーはアプリが応答していないという印象を受けることがあります。

ストリーミングなしのサーバーレンダリング

動的ルートへのナビゲーション体験を改善するには、ストリーミングを使用できます。

ストリーミング

ストリーミングにより、サーバーは動的ルートの一部を全体がレンダリングされるのを待つのではなく、準備ができるとすぐにクライアントに送信できます。これはルートの一部がまだ読み込み中であっても、ユーザーは何かを早く見ることができます。

動的ルートでは、それらを部分的にプリフェッチできることを意味します。つまり、共有レイアウトとローディングスケルトンは事前にリクエストできます。

ストリーミングを使用したサーバーレンダリングの仕組み

ストリーミングを使用するには、ルートフォルダにloading.tsxを作成します。

loading.js特別ファイル
app/dashboard/loading.tsx
TypeScript
export default function Loading() {
  // ルートが読み込み中に表示されるフォールバックUIを追加します。
  return <LoadingSkeleton />
}

バックグラウンドでは、Next.jsは自動的にpage.tsxコンテンツを<Suspense>境界にラップします。プリフェッチされたフォールバックUIはルートが読み込み中に表示され、準備ができたら実際のコンテンツに交換されます。

補足:ネストされたコンポーネントのローディングUIを作成するには<Suspense>も使用できます。

loading.tsxのメリット:

  • ユーザーのための即座のナビゲーションとビジュアルフィードバック。
  • 共有レイアウトは対話的なままで、ナビゲーションは中断可能です。
  • Core Web Vitalsの改善:TTFBFCP、およびTTI

ナビゲーション体験をさらに改善するため、Next.jsは<Link>コンポーネントでクライアント側トランジションを実行します。

クライアント側トランジション

従来、サーバーレンダリングされたページへのナビゲーションは完全なページ読み込みをトリガーします。これは状態をクリア、スクロール位置をリセット、および対話性をブロックします。

Next.jsは<Link>コンポーネントを使用したクライアント側トランジションでこれを回避します。ページをリロードする代わりに、以下を実行して動的にコンテンツを更新します。

  • 共有レイアウトとUIを保持します。
  • 現在のページをプリフェッチされたローディング状態、または利用可能な場合は新しいページに置き換えます。

クライアント側トランジションはサーバーレンダリングされたアプリを_クライアントレンダリングされたアプリのように_感じさせるものです。また、プリフェッチストリーミングと組み合わせると、動的ルートの場合でも高速トランジションが可能になります。

トランジションを遅くさせる原因は何か

これらのNext.js最適化により、ナビゲーションは高速かつレスポンシブになります。ただし、特定の条件下では、トランジションは依然として_遅く_感じられることがあります。以下は一般的な原因とユーザー体験を改善する方法です。

loading.tsxなしの動的ルート

動的ルートにナビゲーションする場合、クライアントは結果を表示する前にサーバーレスポンスを待つ必要があります。これはユーザーにアプリが応答していないという印象を与える可能性があります。

部分的なプリフェッチを有効にし、即座のナビゲーションをトリガー、ルートがレンダリングされている間にローディングUIを表示するために、動的ルートにloading.tsxを追加することをお勧めします。

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

補足:開発モードでは、Next.js Devtoolsを使用してルートが静的か動的かを識別できます。詳細についてはdevIndicatorsを参照してください。

generateStaticParamsなしの動的セグメント

動的セグメントはプリレンダリング可能だが、generateStaticParamsが欠落しているため実行されない場合、ルートはリクエスト時に動的レンダリングにフォールバックします。

generateStaticParamsを追加してルートがビルド時に静的に生成されることを確認します。

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

ただし、プリフェッチを無効にするとトレードオフがあります。

  • 静的ルート はユーザーがリンクをクリックする場合のみ取得されます。
  • 動的ルート はクライアントがナビゲーションできる前にサーバー側でまずレンダリングされる必要があります。

プリフェッチを完全に無効にせずにリソース使用を減らすには、ホバー時のみプリフェッチできます。これはビューポート内のすべてのリンクではなく、ユーザーがより_訪問しやすい_ルートのプリフェッチを制限します。

app/ui/hover-prefetch-link.tsx
TypeScript
'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>はClient Componentで、ルートをプリフェッチする前にハイドレーションされる必要があります。初回訪問時に、大規模なJavaScriptバンドルはハイドレーションを遅延させ、プリフェッチがすぐに開始されるのを防ぎます。

Reactはこれに対応するため、Selective Hydrationを使用し、以下を実行してさらに改善できます。

  • @next/bundle-analyzerプラグインを使用して大規模な依存関係を削除することによってバンドルサイズを特定および削減します。
  • 可能な場合はロジックをクライアントからサーバーに移動します。ガイダンスについてはServer and Client Componentsドキュメントを参照してください。

Native History API

Next.jsでは、ネイティブなwindow.history.pushStateおよびwindow.history.replaceStateメソッドを使用して、ページをリロードせずにブラウザの履歴スタックを更新できます。

pushStatereplaceStateの呼び出しはNext.js Routerに統合され、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) {
    // 例:'/en/about'または'/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) {
    // 例:'/en/about'または'/fr/contact'
    const newPath = `/${locale}${pathname}`
    window.history.replaceState(null, '', newPath)
  }
 
  return (
    <>
      <button onClick={() => switchLocale('en')}>English</button>
      <button onClick={() => switchLocale('fr')}>French</button>
    </>
  )
}