はじめに
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ドキュメントが送られた場合はこちら
- Browsers perform a "hard navigation" when navigating between pages. とあるように、
- 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ファイルのみが送られた場合はこちら
- 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と異なるが、
余談ですがわりと最近「soft navigation」が WICG 等で使われている様です。(こちらの記事を読ませていただきました。) soft navigation 、Hard NavigationでググるとほぼNext.jsに関するコンテンツが出てくるのですが、誰が最初に使い出したのでしょうか...?分かる方がいましたらインターネットの海にご回答を流していただけますと嬉しいです。
という事で調査した内容に入っていきます。
調査方法
今回は以下のことを調査しました。
- SSGページに対するsoft navigationの動作
- SSRページに対するsoft navigationの動作
- SSGページに対するhard navigationの動作
- 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の挙動を見ていきます。
また、soft navigationに関する調査には next/link
を利用しました。
next/link
はデフォルトでviewportに入った時点でprefetchを行うようになっていますが、
今回の検証では、dev toolsのログが見づらくなるため、prefetch=falseにしてhover時のみにprefetchを行うようにしました。
/
へアクセスした際のネットワークログは以下の通りです。
Name | Status | Type | Initiator | Size | Time |
---|---|---|---|---|---|
localhost | 200 | document | Other | 916 B | 38 ms |
webpack-8cac0b4b405cede1.js | 200 | script | (index):0 | 1.2 kB | 57 ms |
framework-a4ddb9b21624b39b.js | 200 | script | (index):0 | 58.1 kB | 57 ms |
main-013d79fd40076fd9.js | 200 | script | (index):0 | 34.3 kB | 56 ms |
_app-64d8ebea9e031e82.js | 200 | script | (index):0 | 728 B | 54 ms |
index-7af61768d8d873e6.js | 200 | script | (index):0 | 2.8 kB | 56 ms |
_buildManifest.js | 200 | script | (index):0 | 1.2 kB | 53 ms |
_ssgManifest.js | 200 | script | (index):0 | 466 B | 9 ms |
content_script_vite-e6cdc5d2.js | 200 | script | content_script_vite.js:1 | 484 B | 3 ms |
favicon.ico | 200 | x-icon | Other | 9.6 kB | 16 m |
htmlドキュメントと、Next.jsの共通ファイルが送られている事が分かります。
次に、/ssg
へのリンクをhoverした際(prefetchした際)のネットワークログは以下の通りでした。
Name | Status | Type | Initiator | Size | Time |
---|---|---|---|---|---|
ssg.json | 304 | fetch | main-013d79fd40076fd9.js:1 | 393 B | 9 ms |
ssg-8cfd9cc48d7a584d.js | 200 | script | main-013d79fd40076fd9.js:1 | 1.1 kB | 4 ms |
prefetchの動作について分かった事
箇条書きで書いていきます。
- ssg.jsonファイルは以下の様な内容で、/ssg のサーバー側で計算された結果が含まれている事が分かる
{
"pageProps": {
"fruits": [
"Elderberry",
"Lemon",
"Date",
"Fig",
"Cherry"
]
},
"__N_SSG": true
}
- 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を実行する
以下のようなリクエストが発生しました。
Name | Status | Type | Initiator | Size | Time |
---|---|---|---|---|---|
ssg.json | 304 | fetch | main-013d79fd40076fd9.js:1 | 244 B | 17 ms |
favicon.ico | 304 | x-icon | Other | 243 B | 8 ms |
soft navigationについて分かった事
箇条書きで書いていきます。
- ssg.jsonに再度リクエストが飛んでいる
- prefetch時と比較してRequest MethodがGETからHEADに変わっていた
- Request Headersにpurpose:prefetchがない ssg.jsonへのリクエストの違いが気になったので、詳しく調査してみました。
ssg.jsonへのリクエストに関する検証
こちらも箇条書きで記述します。
- サーバー起動中に
.next/server/pages/ssg.json
をtest.json
にリネームする
- リネーム以前と変わらず、適切にレスポンスが返ってきました
- ランタイムのグローバル変数として保持されていそう?
- サーバーを一度停止して、
.next/server/pages/ssg.json
をtest.json
にリネームする
- prefetch等があったタイミングで
.next/server/pages/ssg.json
が作成されている事がわかった - Response Headersの
x-nextjs-cache
がMISS
になっていた
このことから、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した際のネットワークログは以下の通りでした。
Name | Status | Type | Initiator | Size | Time |
---|---|---|---|---|---|
ssr-41ff0c23b9c1ab78.js | 200 | script | main-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を実行する
以下のようなリクエストが発生しました。
Name | Status | Type | Initiator | Size | Time |
---|---|---|---|---|---|
ssr.json | 200 | fetch | main-013d79fd40076fd9.js:1 | 380 B | 49 ms |
favicon.ico | 304 | x-icon | Other | 243 B | 4 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
が含まれていない
- Response Headersに
- どうも
.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へアクセスして動作を検証していきます。 ネットワークログは以下の通りになりました。
Name | Status | Type | Initiator | Size | Time |
---|---|---|---|---|---|
ssg | 200 | document | Other | 1.0 kB | 6 ms |
webpack-8cac0b4b405cede1.js | 200 | script | ssg:0 | 1.2 kB | 13 ms |
framework-a4ddb9b21624b39b.js | 200 | script | ssg:0 | 58.1 kB | 27 ms |
main-013d79fd40076fd9.js | 200 | script | ssg:0 | 34.3 kB | 20 ms |
_app-64d8ebea9e031e82.js | 200 | script | ssg:0 | 728 B | 13 ms |
ssg-8cfd9cc48d7a584d.js | 200 | script | ssg:0 | 1.1 kB | 42 ms |
_buildManifest.js | 200 | script | ssg:0 | 1.2 kB | 16 ms |
_ssgManifest.js | 200 | script | ssg:0 | 466 B | 8 ms |
favicon.ico | 304 | x-icon | Other | 243 B | 2 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の動作
ネットワークログは以下の通りになりました。
Name | Status | Type | Initiator | Size | Time |
---|---|---|---|---|---|
ssr | 200 | document | Other | 1.0 kB | 58 ms |
webpack-8cac0b4b405cede1.js | 200 | script | ssr:0 | 1.2 kB | 29 ms |
framework-a4ddb9b21624b39b.js | 200 | script | ssr:0 | 58.1 kB | 84 ms |
main-013d79fd40076fd9.js | 200 | script | ssr:0 | 34.3 kB | 46 ms |
_app-64d8ebea9e031e82.js | 200 | script | ssr:0 | 728 B | 22 ms |
ssr-41ff0c23b9c1ab78.js | 200 | script | ssr:0 | 1.1 kB | 29 ms |
_buildManifest.js | 200 | script | ssr:0 | 1.2 kB | 26 ms |
_ssgManifest.js | 200 | script | ssr:0 | 466 B | 11 ms |
Otherfavicon.ico | 304 | x-icon | Other | 243 B | 2 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以下のファイルが 結構多かったのも驚きでした。 今後も学び、アウトプットしていきたいと思います。ありがとうございました。