各レンダリング方式でのNavigationの具体的な動作を調査する(Next.js Pages Router)

February 11, 2025

はじめに

Next.jsのPages Routerにおける各レンダリング方式でのナビゲーション(Navigation) を深掘りしたく、リバースエンジニアリング的なアプローチで調査してみました。
個人的には興味深く、面白い事を知れたので、自身への備忘録的に残しておきます。どんな風に動作しているかちょっと気になる方の一助になればと思います。よろしくお願いします。

Next.js の Navigation とは

Next.js Pages Router文脈でのNavigationとは主にページ遷移の手法(URLの変更とそれに伴うレンダリング)を指しており、大きく2つの分類があります。 Pages Routerのドキュメントにはその用語に関する記載が見つかりませんでしたが、こちらのApp Router のドキュメントには記載があったので、それを引用しつつ箇条書きで以下に整理します。

  • Hard Navigation
    • Browsers perform a "hard navigation" when navigating between pages. とあるように、<a>タグでの遷移やブラウザの機能によるページ遷移を指している
      • より具体的にいうと、クライアントにhtmlドキュメントが送られた場合はこちら
  • Soft Navigation
    • The Next.js App Router enables "soft navigation" between pages, ensuring only the route segments that have changed are re-rendered (partial rendering). とあり、少しPages Routerと異なるが、next/link等を用いた際のページ遷移を指す
      • より具体的にいうと、クライアントにhtmlドキュメントが送られず、javascriptファイルとjsonファイルのみが送られた場合はこちら

余談ですがわりと最近「soft navigation」が WICG 等で使われている様です。(こちらの記事を読ませていただきました。) soft navigation 、Hard NavigationでググるとほぼNext.jsに関するコンテンツが出てくるのですが、誰が最初に使い出したのでしょうか...?分かる方がいましたらインターネットの海にご回答を流していただけますと嬉しいです。

という事で調査した内容に入っていきます。

調査方法

今回は以下のことを調査しました。

  1. SSGページに対するsoft navigationの動作
  2. SSRページに対するsoft navigationの動作
  3. SSGページに対するhard navigationの動作
  4. SSRページに対するhard navigationの動作

それを調査するために、以下の5つのページを用意して、Navigationの動作を調査しました。

  • /
    • ナビゲーション周りに関連する動作を調査するため
  • /ssg
    • SSGの動作を調査するため
  • /ssr
    • SSRの動作を調査するため
  • /ssg-with-interaction
    • インタラクション要素のあるSSGの動作を調査するため
  • /ssr-with-interaction
    • インタラクション要素のあるSSRの動作を調査するため

調査に利用したレポジトリはこちらです。

build時のログは以下の様な感じでした。

Route (pages)                             Size     First Load JS
┌ ○ /                                     2.42 kB        94.8 kB
├   /_app                                 0 B            92.4 kB
├ ○ /404                                  189 B          92.6 kB
├ ● /ssg                                  422 B          92.8 kB
├ ● /ssg-with-interaction                 450 B          92.9 kB
├ ƒ /ssr                                  418 B          92.8 kB
└ ƒ /ssr-with-interaction                 382 B          92.8 kB
+ First Load JS shared by all             92.4 kB
  ├ chunks/framework-a4ddb9b21624b39b.js  57.5 kB
  ├ chunks/main-013d79fd40076fd9.js       33.8 kB
  └ other shared chunks (total)           1.08 kB

○  (Static)   prerendered as static content
●  (SSG)      prerendered as static HTML (uses getStaticProps)
ƒ  (Dynamic)  server-rendered on demand

余談ですが、各ページのFirst Loadはshared by allも含んだサイズを示してくれており、親切だと感じました。

では、調査結果をまとめていきます。

SSG ページに対する soft navigation の動作

/にアクセスして、ナビゲーション周りを調べていきます。まずはprefetchの挙動を見ていきます。 Top page screen shot

また、soft navigationに関する調査には next/linkを利用しました。 next/linkはデフォルトでviewportに入った時点でprefetchを行うようになっていますが、 今回の検証では、dev toolsのログが見づらくなるため、prefetch=falseにしてhover時のみにprefetchを行うようにしました。

/へアクセスした際のネットワークログは以下の通りです。

NameStatusTypeInitiatorSizeTime
localhost200documentOther916 B38 ms
webpack-8cac0b4b405cede1.js200script(index):01.2 kB57 ms
framework-a4ddb9b21624b39b.js200script(index):058.1 kB57 ms
main-013d79fd40076fd9.js200script(index):034.3 kB56 ms
_app-64d8ebea9e031e82.js200script(index):0728 B54 ms
index-7af61768d8d873e6.js200script(index):02.8 kB56 ms
_buildManifest.js200script(index):01.2 kB53 ms
_ssgManifest.js200script(index):0466 B9 ms
content_script_vite-e6cdc5d2.js200scriptcontent_script_vite.js:1484 B3 ms
favicon.ico200x-iconOther9.6 kB16 m

htmlドキュメントと、Next.jsの共通ファイルが送られている事が分かります。

次に、/ssgへのリンクをhoverした際(prefetchした際)のネットワークログは以下の通りでした。

NameStatusTypeInitiatorSizeTime
ssg.json304fetchmain-013d79fd40076fd9.js:1393 B9 ms
ssg-8cfd9cc48d7a584d.js200scriptmain-013d79fd40076fd9.js:11.1 kB4 ms
prefetchの動作について分かった事

箇条書きで書いていきます。

  • ssg.jsonファイルは以下の様な内容で、/ssg のサーバー側で計算された結果が含まれている事が分かる
{
  "pageProps": {
    "fruits": [
      "Elderberry",
      "Lemon",
      "Date",
      "Fig",
      "Cherry"
    ]
  },
  "__N_SSG": true
}
  • Sourcesを見ると以下の様な感じ Sources
  • ssg-8cfd9cc48d7a584d.jsについて
    • リクエストされているURL: /_next/static/chunks/pages/ssg-8cfd9cc48d7a584d.js
    • サーバー側の .next/static/chunks/pages/ssg-8cfd9cc48d7a584d.jsに存在する
    • client側を見ると _buildManifest.js 内で "/ssg": ["static/chunks/pages/ssg-8cfd9cc48d7a584d.js"] のように記述がある事もわかった。
  • ssg.jsonについて
    • リクエストされているURL: /_next/data/EUrsPW3d-AuI1h55CF7hP/ssg.json
    • サーバー側の .next/server/pages/ssg.json に存在し、内容も同じ。
    • 気になるResponse Headers
      • x-nextjs-cache:HIT
      • x-nextjs-matched-path:/ssg
      • x-nextjs-prerender:1
    • 気になるRequest Headers
      • purpose:prefetch
      • x-nextjs-data:1
  • prefetch時のリクエストはmainファイルから発生している
    • prefetchの機能はmainファイルが持っている
    • main jsファイルのリクエストをブロックするとprefetchは発生しなかった
  • ssgのjsonファイルはhoverの度にリクエストが発生するが、jsファイルは一度のみ

上記の内容から、SSGの場合はprefetch時に、既にbuild時に生成済みのファイルがクライアントに送られている事が分かりました。
また、nextjs固有のcacheに関する仕組みもありそうです(この後詳しく検証しています)。次にsoft navigationを実行した際の動作をまとめます。

SSGページへのsoft navigationを実行する

以下のようなリクエストが発生しました。

NameStatusTypeInitiatorSizeTime
ssg.json304fetchmain-013d79fd40076fd9.js:1244 B17 ms
favicon.ico304x-iconOther243 B8 ms
soft navigationについて分かった事

箇条書きで書いていきます。

  • ssg.jsonに再度リクエストが飛んでいる
    • prefetch時と比較してRequest MethodがGETからHEADに変わっていた
    • Request Headersにpurpose:prefetchがない ssg.jsonへのリクエストの違いが気になったので、詳しく調査してみました。
ssg.jsonへのリクエストに関する検証

こちらも箇条書きで記述します。

  1. サーバー起動中に .next/server/pages/ssg.jsontest.jsonにリネームする
  • リネーム以前と変わらず、適切にレスポンスが返ってきました
  • ランタイムのグローバル変数として保持されていそう?
  1. サーバーを一度停止して、 .next/server/pages/ssg.jsontest.jsonにリネームする
  • prefetch等があったタイミングで .next/server/pages/ssg.json が作成されている事がわかった
  • Response Headersの x-nextjs-cacheMISS になっていた

このことから、soft navigation時のサーバー側の計算結果取得(jsonファイルの取得)は念の為サーバー側のキャッシュを確認している可能性が高そうです。

また、ssg.jsonへのnetwork requestをblockした状態(prefetchをブロック)で、/ssgへのリンクをクリックすると、自動的にhard navigationに切り替わっていました。 これについて少し考察すると、まず、SSGによって生成されたhtmlには _NEXT_DATA_ にサーバー側の計算結果が含まれるため、soft navigation時のようにnext/dataの取得が必要ありません。 そのためhard navigationに切り替えて試行するのはかなり有効なアプローチに見えます。

次に、SSRページに対するsoft navigationの動作を調査していきます。

SSR ページに対する soft navigation の動作

/ssrへのリンクをhoverした際のネットワークログは以下の通りでした。

NameStatusTypeInitiatorSizeTime
ssr-41ff0c23b9c1ab78.js200scriptmain-013d79fd40076fd9.js:1(disk cache)1 ms

prefetchについて分かった事

  • request URL:/_next/static/chunks/pages/ssr-41ff0c23b9c1ab78.js
    • サーバー側の.next/static/chunks/pages/ssr-41ff0c23b9c1ab78.jsに存在している
  • SSGの時と比較して、jsonファイルがクライアントに送られていない
    • サーバー側の計算についてはprefetchのタイミングで行われていない

次にsoft navigationを実行した際の動作をまとめます。

SSRページへのsoft navigationを実行する

以下のようなリクエストが発生しました。

NameStatusTypeInitiatorSizeTime
ssr.json200fetchmain-013d79fd40076fd9.js:1380 B49 ms
favicon.ico304x-iconOther243 B4 ms
SSRページへのsoft navigationについて分かった事
  • request URL: /_next/data/EUrsPW3d-AuI1h55CF7hP/ssr.json
  • ssr.jsonをこのタイミング(soft navigationによるページ遷移)で取得している
    • 内容は以下の通り:
{
    "pageProps": {
        "fruits": [
            "Fig",
            "Date",
            "Lemon",
            "Grape",
            "Cherry"
        ]
    },
    "__N_SSP": true
}
  • SSGの時との比較:
    • Response Headersに x-nextjs-cache, x-nextjs-matched-path, x-nextjs-prerender が含まれていない
  • どうも .next/server/pages/ssr.jsが /ssrのサーバー側の処理を行う役割を持っていそう
    • パッとみた感じ.next/static/chunks/pages がClient側に渡すソースで、.next/server/pages/ がServer側での処理を行う様子
  • "/ssr": ["static/chunks/pages/ssr-41ff0c23b9c1ab78.js"]という記述が _buildManifest.js にある

soft navigationに関する調査で分かった事のまとめ

  • SSGの場合はprefetch時に、既にbuild時に生成済みのjsonファイルがクライアントへ送られている
    • 何らかの理由でjsonファイルがサーバー側になかった場合は、初回リクエスト時に生成して、以後サーバー内でキャッシュする
  • SSRの場合はprefetch時にjsonファイルはクライアントに送られない
    • soft navigation実行時に、サーバー側で計算し、その結果をjsonファイルとしてクライアントに送る
    • その結果をクライアントにキャッシュする等は行われていない(リクエストの都度サーバー側で計算が行われる理解だったので、元々の理解からも正しい)
    • 実際soft navigationを実行する度にコンテンツが変わるので、キャッシュされていない事がわかる
  • .next/static/chunks/pages がClient側に渡すソースで、.next/server/pages/ がServer側での処理を行うソースである事

次にhard navigation周りの動作を見ていきます。

SSGページに対するhard navigationの動作

hard navigationについてはブラウザから直接URLへアクセスして動作を検証していきます。 ネットワークログは以下の通りになりました。

NameStatusTypeInitiatorSizeTime
ssg200documentOther1.0 kB6 ms
webpack-8cac0b4b405cede1.js200scriptssg:01.2 kB13 ms
framework-a4ddb9b21624b39b.js200scriptssg:058.1 kB27 ms
main-013d79fd40076fd9.js200scriptssg:034.3 kB20 ms
_app-64d8ebea9e031e82.js200scriptssg:0728 B13 ms
ssg-8cfd9cc48d7a584d.js200scriptssg:01.1 kB42 ms
_buildManifest.js200scriptssg:01.2 kB16 ms
_ssgManifest.js200scriptssg:0466 B8 ms
favicon.ico304x-iconOther243 B2 ms

htmlドキュメントと、このページ固有のjsファイル、Next.jsの共通ファイルが送られている事が分かる。

分かった事
  • ssg.html以外のリクエストをブロックしても、画面描画には問題ない
    • インタラクション要素、Next.jsの機能に依存する要素がないため
    • 試しに、/ssg-with-interactionで同じ様にブロックしてみると、インタラクション要素が機能しなくなった。
  • clientへ送られたssg.htmlは .next/server/pages/ssg.htmlに存在している
  • ssg.htmlについて気になった事
    • Response Headersに以下のようなものが含まれていた
    • x-nextjs-cache:HIT
    • x-nextjs-prerender:1
    • 試しに、prefetchに取得されるssg.jsonと同じような事を試すと、ほぼ同じ様な結果が得られた。 また、_NEXT_DATA_ 内容を以下に記載します。
{
  "props": {
      "pageProps": {
          "fruits": [
              "Grape",
              "Fig",
              "Elderberry",
              "Honeydew",
              "Date"
          ]
      },
      "__N_SSG": true
  },
  "page": "/ssg",
  "query": {
  },
  "buildId": "EUrsPW3d-AuI1h55CF7hP",
  "isFallback": false,
  "isExperimentalCompile": false,
  "gsp": true,
  "scriptLoader": [
  ]
}

SSRページに対するhard navigationの動作

ネットワークログは以下の通りになりました。

NameStatusTypeInitiatorSizeTime
ssr200documentOther1.0 kB58 ms
webpack-8cac0b4b405cede1.js200scriptssr:01.2 kB29 ms
framework-a4ddb9b21624b39b.js200scriptssr:058.1 kB84 ms
main-013d79fd40076fd9.js200scriptssr:034.3 kB46 ms
_app-64d8ebea9e031e82.js200scriptssr:0728 B22 ms
ssr-41ff0c23b9c1ab78.js200scriptssr:01.1 kB29 ms
_buildManifest.js200scriptssr:01.2 kB26 ms
_ssgManifest.js200scriptssr:0466 B11 ms
Otherfavicon.ico304x-iconOther243 B2 ms
分かった事
  • ssr.html以外のリクエストをブロックしても、画面描画には問題ない
  • ssr.htmlはサーバー側に存在せず、リクエストがあった後も生成されない
    • .next/server/pages/ssr.jsがリクエストの都度、htmlファイルを生成しているように見える また、_NEXT_DATA_ 内容を以下に記載します。
{
    "props": {
        "pageProps": {
            "fruits": [
                "Date",
                "Honeydew",
                "Lemon",
                "Kiwi",
                "Grape"
            ]
        },
        "__N_SSP": true
    },
    "page": "/ssr",
    "query": {
    },
    "buildId": "EUrsPW3d-AuI1h55CF7hP",
    "isFallback": false,
    "isExperimentalCompile": false,
    "gssp": true,
    "scriptLoader": [
    ]
}

hard navigationに関する調査で分かった事のまとめ

  • SSG、SSR共に、htmlファイルはサーバー側で生成される
    • SSGは、build時に生成されたhtmlファイルがクライアントに送られる
  • もしサーバー側になかった場合、初回リクエスト時に生成して、以後サーバー内でキャッシュする
    • SSRは、リクエストの都度、サーバー側で生成される

hard navigationに関しても、SSGページのキャッシュの仕組みを知れたのが興味深かったです。また、hydrationに関する振る舞いも本当に一部ですが見る事ができました。

まとめ

公式ドキュメントや、日々Next.jsを触っている中で何となく理解して使っていましたが、こうして詳細に動作を追う事で、それぞれのレンダリング方式のpros,consがより具体的に理解出来て良かったです。 いろいろ学びがありましたが、個人的にはSSGページのキャッシュの仕組みと .nextのディレクトリ構造が興味深かったです。また、かなり最小構成のつもりで作成したサンプルプロジェクトの.next以下のファイルが 結構多かったのも驚きでした。 今後も学び、アウトプットしていきたいと思います。ありがとうございました。