💣

Link コンポーネントのプリフェッチをオフにするべきパターン

2024/10/21に公開

環境情報

Next.js の 14.2.15 バージョンの App Router で確認をしています。

調べるモチベーション

プリフェッチをする理由は、ナビゲーション時の待ち時間軽減が大きいと思います。ユーザーの使用している端末の通信速度が遅い場合でもプリフェッチによって、ナビゲーションによる待ち時間を減らせる可能性があります。ただ、ナビゲーションする可能性のあるコンポーネントを全てプリフェッチするため、ナビゲーションしないコンポーネントに対するプリフェッチは、ユーザーからすると不要なリソースの読み込みになります。また、サーバーとしても不要なリソースのリクエストを捌くことになります。

ここはトレードオフなので、アプリケーションの特性によって判断が必要な部分です。

ただ、Next.js のプリフェッチはデフォルトでオンです。デフォルトの設定の場合、ガンガンとプリフェッチをしていきます。ブラウザのネットワークのコンソールに大量のプリフェッチ処理が流れているのを見て、ふと...あれ、これでいいんだっけ?と思って調べたのが今回のモチベーションです。

Next.js は HTML 要素の <a> タグを拡張して、ルート間のプリフェッチとクライアント側のナビゲーションを提供する Link コンポーネントを用意しています。

(参考:Link コンポーネント)

https://nextjs.org/docs/app/api-reference/components/link

<a> タグの拡張は link.tsx で実装されています。

(参考:link.tsx)
https://github.com/vercel/next.js/blob/2451f52bc8aba654d38db8373f7138839007bc71/packages/next/client/link.tsx#L436-L438

ちなみに Next.js はクライアント側のナビゲーションを useRouter という Hooks を使用しても実装できるようにしています。
https://nextjs.org/docs/app/api-reference/functions/use-router

Link コンポーネントを扱うと何となく触れておきたくなりました。この疑問についてですが、基本的には Link コンポーネントを使用する、のだと思います。

Recommendation: Use the <Link> component for navigation unless you have a specific requirement for using useRouter.
https://nextjs.org/docs/app/api-reference/functions/use-router

公式ドキュメントにも上記の通り、特別な要件がない限りは Link コンポーネントを使用してね、と記載があります。

また、link.tsx の実装を確認すると内部的には Link コンポーネントは useRouter を使用しています。そのため、Link コンポーネントと useRouter によるナビゲーションの実現方法は同じソフトナビゲーションになるため、それであれば <a> タグを使用して SEO 的にも綺麗な Link コンポーネントを使用するのが良いと思います。

特定のイベントや時間などにフックさせてナビゲーションをしたい、という固有の要件であれば Link コンポーネントで実装できないため、そういった場合に useRouter を使用するのが良いと思います。

本題に戻ります。

プリフェッチの仕様の整理

Link コンポーネントがユーザーのビューポートに入ったときに、href としてリンクされたルートとそのデータをバックグラウンドでプリフェッチして読み込みます。
以下の場合、/sample のコンポーネントとそのデータがプリフェッチされます。

import Link from "next/link";
export default function Home() {
  return (
    <ul>
      <li><Link href={"/sample"}>sample</Link></li>
    </ul>
  );
}

また、ビューポートに入ったタイミングで実行されるプリフェッチは、本番環境でのみ有効です。開発環境でのプリフェッチはリソースの無駄だから、という理由で制御されています。

(参考:プリフェッチの実行制御)
https://github.com/vercel/next.js/blob/fa42c3461fdc8f02b6abf9037bfa658ad523f5a1/packages/next/client/link.tsx#L525-L528

これらの仕様はデフォルトでオンになっています。

細かいプリフェッチの仕様

以下は細かいですが、個人的に気になったプリフェッチの仕様です。

  • ブラウザをリフレッシュした場合は再度プリフェッチされる?
  • プリフェッチした情報が古くなった場合は、古い情報がナビゲーション先で使用される?
  • ナビゲーション先で使用されているイメージ(<image />)はプリフェッチの対象になる?

以降、一問一答形式で記載していきますが、Next.js の挙動は設定などによってブレるため、あくまでも私が確認した方法ではこうだった、という話になります。

ブラウザをリフレッシュした場合は再度プリフェッチされる?

再度プリフェッチされます。

プリフェッチの結果は、ルーターキャッシュに保存されますが、ルーターキャッシュはページのリフレッシュでクリアされるため、再度プリフェッチされます。

(参考:ルーターキャッシュ)
https://nextjs.org/docs/app/building-your-application/caching#router-cache

ただ、プリフェッチはされるのですが、サーバー側からフルルートキャッシュ、あるいは、データキャッシュが返されるため、プリフェッチの処理は軽いです。

以下、挙動を確認したサンプル実装です。

import Link from "next/link";
export default function Home() {
  return (
    <ul>
      <li><Link href={"/client-a"}>client-a</Link></li>
      <li><Link href={"/client-b"}>client-b</Link></li>
      <li><Link href={"/server-a"}>server-a</Link></li>
      <li><Link href={"/server-b"}>server-b</Link></li>
    </ul>
  );
}

上記の実装だと初回のレンダリングでは全てのコンポーネントのプリフェッチが実行され、サーバー側から 200 ステータスのレスポンスを受け取ります。

ページのリフレッシュによって、再度プリフェッチは実行されますが、サーバー側からのステータスは 304 (Not Modified) です。

(参考:client-a コンポーネントの再度のプリフェッチのステータスは 304)

ただし、これはプリフェッチ対象のコンポーネントが static だった場合です。dynamic なコンポーネントの場合、フルルートキャッシュ/データキャッシュが効かないため、ページのリフレッシュによって実行されるプリフェッチは、1から処理されることになります。

例えば、以下のようなコンポーネントのキャッシュは効かないです。

export const dynamic = 'force-dynamic'
export default async function ServerComponent() {
    const randomNumber = Math.random()
    return (
        <div>
            {`Server Component: ${randomNumber}`}
        </div>
    );
}

ページのリフレッシュによって実行されるプリフェッチでは、200 ステータスになります。

(参考:再度のプリフェッチでも server-a コンポーネントのステータスは 200)

ちなみに、コンポーネントが dynamic or static かどうかはビルド時にログに表示されます。

Route (app)                              Size     First Load JS
┌ ○ /                                    6.96 kB        94.1 kB
├ ○ /_not-found                          875 B            88 kB
├ ƒ /dynamic                             177 B          92.4 kB
└ ○ /static                              178 B          92.4 kB

プリフェッチした情報が古くなった場合は、古い情報がナビゲーション先で使用される?

ナビゲーションの直前にあるマウスオーバー/タッチスタートのイベントでプリフェッチが実行されるため、ナビゲーション先で古い情報が使用されることはありません。ルーターキャッシュが効いている期間はプリフェッチは実行されず、実行されたとしても、サーバー側からフルルートキャッシュ、あるいは、データキャッシュが返されるため、プリフェッチの処理は軽いケースが大半です。

(参考:onMouseEnter
https://github.com/vercel/next.js/blob/56cf916714b48f6bc9ed6d209936b6df549b9671/packages/next/src/client/link.tsx#L643

(参考:onTouchStart
https://github.com/vercel/next.js/blob/56cf916714b48f6bc9ed6d209936b6df549b9671/packages/next/src/client/link.tsx#L683

ナビゲーション先で使用されているイメージはプリフェッチの対象になる?

イメージ(<image />)はプリフェッチの対象外です。

以下が挙動を確認したサンプル実装で、プリフェッチ対象のコンポーネントにイメージを仕込んでいます。

import Image from "next/image";

export default async function ServerComponent() {
  const randomNumber = Math.random()
  return (
    <>
      <div>
        {`Server Component: ${randomNumber}`}
      </div>
      <Image
        src={`/icon_m_1101001_00.webp`}
        width={50}
        height={50}
        alt="character icon"
      />
    </>
  );
}

プリフェッチは、コンポーネントとそのコンポーネントで使用しているデータまでが対象であり、イメージは対象外でした。

以下のようなパターンは当然ながら気をつける必要はありますが、ここまで仕様を確認した範囲ではプリフェッチ対象を static なコンポーネントにしてさえおけば、危ないプリフェッチになることはあまりないように思います。

  • プリフェッチ対象が大量にある場合
  • プリフェッチ対象のデータが大きい場合

ただ、dynamic なコンポーネントがプリフェッチ対象になる場合は、フルルートキャッシュ/データキャッシュが効かないため、何度もプリフェッチの処理が走る可能性があります。そのため、プリフェッチをオフにすることを検討していいかもしれません。

そもそも dynamic なコンポーネントをプリフェッチする意義は薄いように思います。

以下の実装でプリフェッチはオフにすることができます。

(参考:prefetch をオフにする)

import Link from 'next/link'
 
export default function Page() {
  return (
    <Link href="/dashboard" prefetch={false}>
      Dashboard
    </Link>
  )
}

まとめ

プリフェッチを有効にする、しないの判断は色々な要素が絡みます。今回、記載したような Link コンポーネントのプリフェッチをオフにするべきパターンでも、オンのままでいい場合も十分考えられます。アプリケーションの特定に合った判断が必要というやつですね。

Discussion