🚀

Next.js の App Router の prefetch 指定と実際の挙動が想定外すぎて辛い

に公開

カバー画像

READYFOR Advent Calendar 2025 の5日目の記事です

問題です

Next.js(投稿時点ではv16.0.7)の Link コンポーネント が受け取る prefetch propsは、以下のような指定を受け入れるように設計されています。

<Link>
<Link prefetch={true}>
<Link prefetch={false}>
<Link prefetch="auto">
<Link prefetch={null}>

問1
上記の指定のうち、プリフェッチが行われるような prefetch の指定を全て列挙しなさい。

問2
上記の指定のうち、プリフェッチのタイミングを「画面内に入る、もしくはホバーをした時」という制御が入っているような prefetch の指定を全て列挙しなさい。

解説です

大体はドキュメントに書いてあることなので、ドキュメントをよく読めている人は間違えないかと思います。

ですが、APIの見た目からこうだろうという推測に頼りすぎてしまったり、ドキュメントを斜め読みする(私のような)人は、きっとこのprefetchの想定外の挙動にびっくりすることになります。

問1の解説

プリフェッチが発生する指定は、prefetch={false}以外の全てです。つまり、下記はプリフェッチが走ります。

<Link>
<Link prefetch={true}>
<Link prefetch="auto">
<Link prefetch={null}>

理由としては、そういう実装であり、ドキュメントにもそう書かれているからです。

正直、このAPIインターフェースは人に優しくないように思います。<Link prefetch={null} />でプリフェッチが発生することを想像できる人は存在するのでしょうか?

デフォルトでは常にプリフェッチが走りますよという仕様については、そこまで変ではないかと思います。ですが、prefetch?: "auto" | booleanで良いものに、わざわざnullを足してそれを"auto"と同じものとして扱うというのは、さすがに無理がある気がする...

そもそも、デフォルトがプリフェッチという挙動をするのであれば、input要素のdisabledと同じインターフェースをリスペクトして、<Link noprefetch>という見た目にしてもよかった気がします。

問2の解説

Next.jsでは、false指定以外の全てで、「viewportに入る、もしくはホバーされた時にプリフェッチする」という記載があります。なので、下記全て同じタイミングでプリフェッチが実行されます。

<Link>
<Link prefetch={true}>
<Link prefetch="auto">
<Link prefetch={null}>

「わざわざ"auto"trueに分けられているわけだし、見た目的に "auto" 指定のみがviewportやhoverでプリフェッチをする特殊な制御を行なって、trueの場合は最速でプリフェッチするんだろうな〜」という予想ができそうですが、そういうわけではありません。

理由としては、そういう実装であり、ドキュメントにもそう書かれているからです。

正直、このAPIインターフェースは人に優しく(ry

true"auto"の違いは、プリフェッチをするかどうかの制御の違いではありません。違いは、「リンク先に応じてプリフェッチする内容を変えるかどうか」です。

先にprefetch="auto"の方を解説します。"auto"指定では、リンク先が静的ルートの場合はすべてのページの内容を取得しますが、動的ルートの場合は直近のloading.js境界[1]までのデータがprefetchされます。この挙動は特に動的なコンテンツの計算が重いようなページで有効です。viewportやhoverされると、そのルートにおいて必ず必要になるlayout.js, loading.jsなどがプリフェッチされることになります。これによって、閲覧するかどうかわからないdynamicなページをサーバ側が無駄に計算することはなく、ユーザーがいざ遷移しようとした時にはプリフェッチされたlayout.jsとloading.jsを即座に表示することができ、その間にpage.jsのレンダリングをすることができます。

true 指定の場合は、リンク先に関係なく、すべてのコンテンツをプリフェッチします。

どうあるべきかの感想

個人的に好きなセッションがあるのでこちらから言葉を拝借します。Rich Hickey 氏 の Simple Made Easyというセッションがあるのですが、prefetchの複雑怪奇な設計が、このセッションで言われているcomplectした(複数の役割が毛玉のように絡まる)状態になっています。

既存のprefetchが持っている知識を最小単位に分割すると、このように分解することができます。

  • プリフェッチが有効か無効か
  • プリフェッチをする挙動はデフォルトで有効であること
  • プリフェッチする内容をリンク先に応じて変えるかどうか
  • プリフェッチする内容をリンク先に応じて変えることがデフォルトで有効になっていること

であれば、何もprefetch という名称に複数の役割をねじ込む必要はなく、素直に分ければ良いのです。その組み合わせは利用者が決めれば良いのです。

構造としては、"プリフェッチしない" | "プリフェッチする"であり、プリフェッチする場合はその戦略として"リンク先に応じて柔軟に取得範囲を切り替える" | "全て取得する"という構造をそのまま作れば良いだけです。このようなインターフェースであれば迷うことはないと思います。

/**
 * ここにいい感じの説明を書いておく
 */
type LinkProps =
  | {
      prefetch: false;
    }
  | {
      prefetch?: true;
      prefetchMode?: "auto" | "full";
    };

書き換えた場合のイメージはこちらです。

<Link>
<Link prefetch={true}>
<Link prefetch={false}>
<Link prefetchMode="auto">
<Link prefetchMode="full">

まとめ

  • App Router の prefetch は見た目から推測できる直感的な振る舞いに反している
  • とはいえ、文句を言っても仕方ないので、ドキュメントをよく読み検証しましょう
  • 普段の開発からこういう複雑になったロジックやAPIに注意を向け、適切に分解して読み手に優しい設計を心がけましょう
脚注
  1. 元のドキュメントでは、「For dynamic routes, the partial route down to the nearest segment with a loading.js boundary will be prefetched.」と書かれていますが、nearestというのがどこを指しているかは正直わかりませんでした。検証をしていた際も、loading.js boundaryがネストしてた時に、より末端に近いloading.jsが見られるパターンと、よりルートに近いloading.jsが見られるパターンと複数あり、あまりはっきりとした挙動は理解できていません。(だれか教えてほしい) ↩︎

READYFORテックブログ

Discussion