コンテンツにスキップ
App Routerはじめにリンクとナビゲーション

リンクとナビゲーション

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
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 はユーザーが jamais 訪問しない可能性のあるルートに対して不要なサーバー作業を回避します。しかし、ナビゲーションの前にサーバーの応答を待つと、ユーザーはアプリが応答していないように感じることがあります。

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 の利点

  • ユーザーへの即時ナビゲーションと視覚的なフィードバック。
  • 共有レイアウトはインタラクティブなままで、ナビゲーションは中断可能です。
  • Core Web Vitals の改善:TTFBFCP、およびTTI

ナビゲーションエクスペリエンスをさらに向上させるために、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' : ''}`} />
  )
}

ヒントを「デバウンス」するには、初期アニメーション遅延 (例: 100ms) を追加し、不可視 (例: 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> は Client Component であり、ルートをプリフェッチできるようにするにはハイドレーションが必要です。初回訪問時、大きな JavaScript バンドルはハイドレーションを遅延させ、プリフェッチがすぐに開始されない可能性があります。

React は選択的ハイドレーションでこれを軽減します。さらに、以下によって改善できます。

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

ネイティブ履歴API

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

pushState および replaceState の呼び出しは 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>
    </>
  )
}

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