🧊

静的サイトジェネレータとしてのNext.js App Router

ふと気になったので試してみた。

これまでも、ただのBetter webpackとしてしかNext.jsのことを見てなかったり、とことんマイノリティなユースケースに生きてる。

やりたかったこと

  • クライアントでJSを使わない、複数の静的なHTML+CSSを出力したい
    • LinkImageもなにも使わない
  • テンプレート言語としてReactもといJSX
  • そして、CMSやローカルのコンテンツを取得する部分に、RSCの書き味を使えないか
    • Markdownを変換したり、ハイライトしたり

やる前からなんとなく結果は見えてるけど、まあ一応ね・・・。

コード

leaysgur/next-app-router-as-ssg https://github.com/leaysgur/next-app-router-as-ssg

outディレクトリもまとめてコミットしてある。

特記事項としては、

  • next.config.ts: output: "export"
  • app/lib.ts: 記事リストを返す担当、3秒まって配列をただ返すだけ
  • app/layout.tsx: 上記の記事リストを取得し、リンクとして表示
  • app/posts/[path]/page.tsx: generateStaticParams()で上記の記事リストを使う

これだけ。各ページの中身はただの文字列のみ。

ビルドログ

リポジトリのREADMEにも貼ったけど、こうなった。

> next build

   ▲ Next.js 15.1.6

   Creating an optimized production build ...
 ✓ Compiled successfully
 ✓ Linting and checking validity of types
 ✓ Collecting page data
 ✓ Generating static pages (9/9)
 ✓ Collecting build traces
 ✓ Exporting (3/3)
 ✓ Finalizing page optimization

Route (app)                              Size     First Load JS
┌ ○ /                                    142 B           106 kB
├ ○ /_not-found                          979 B           106 kB
├ ○ /about                               142 B           106 kB
└ ● /posts/[path]                        142 B           106 kB
    ├ /posts/hello
    ├ /posts/world
    └ /posts/export
+ First Load JS shared by all            105 kB
  ├ chunks/4bd1b696-20882bf820444624.js  52.9 kB
  ├ chunks/517-8339dfdf94467857.js       50.5 kB
  └ other shared chunks (total)          1.88 kB


○  (Static)  prerendered as static content
●  (SSG)     prerendered as static HTML (uses generateStaticParams)

9ページってどこからきた?1+1+3ページしか定義した覚えないけど・・・。

Not foundは用意しなかったから、デフォルトのリッチなやつがサイズに現れてるんかな?

ともあれ、First load JSはやっぱ残るのだなあ・・・ってのが、想定通りでありつつ、やっぱ思うようには使えんな〜って感想。

outディレクトリをホストして、実際にブラウザでページ表示してみても、JSは読み込まれてた。 webpackの残骸と、React一式と、Nextのベース一式って感じのコードが、全ページで同じ顔ぶれだった。

生成されたHTMLとしてはJSなしで完成系になってるので、単にscriptタグを削除したものが欲しかったものではある。

他には、GeneratingExportingのフェーズで、3秒待つコードが効いてる感じの挙動をしていたのが気になった。 各ページを生成する度に、同一の内容を取得しにいってその都度待つ・・・みたいな。

やっぱ手元でフルビルドするタイプのアーキテクチャをやる場合、データ取得はもっと手前の処理として分離したほうがいいし、このへんはやはりAstroのほうが先を行ってる。

ファイルツリー

いちおう。

❯ tree out
out
├── 404.html
├── _next
│   ├── 4hiqT6fDHTg34dhutjwrp
│   └── static
│       ├── 4hiqT6fDHTg34dhutjwrp
│       │   ├── _buildManifest.js
│       │   └── _ssgManifest.js
│       ├── chunks
│       │   ├── 4bd1b696-20882bf820444624.js
│       │   ├── 517-8339dfdf94467857.js
│       │   ├── app
│       │   │   ├── _not-found
│       │   │   │   └── page-0043d75de3bc49f5.js
│       │   │   ├── about
│       │   │   │   └── page-09e0427d9a0a5bd7.js
│       │   │   ├── layout-100e8db8e6dde64d.js
│       │   │   ├── page-8544ae31a08bfac9.js
│       │   │   └── posts
│       │   │       └── [path]
│       │   │           └── page-8a38e76c1efd65c2.js
│       │   ├── framework-6b27c2b7aa38af2d.js
│       │   ├── main-7f426374dde59c9a.js
│       │   ├── main-app-b6528357a12ccf2c.js
│       │   ├── pages
│       │   │   ├── _app-d23763e3e6c904ff.js
│       │   │   └── _error-9b7125ad1a1e68fa.js
│       │   ├── polyfills-42372ed130431b0a.js
│       │   └── webpack-db0a529a99835594.js
│       └── css
│           └── 6adfa2c2b9798cd6.css
├── about.html
├── about.txt
├── favicon.ico
├── index.html
├── index.txt
└── posts
    ├── export.html
    ├── export.txt
    ├── hello.html
    ├── hello.txt
    ├── world.html
    └── world.txt

.htmlと一緒に出力される.txtは何なんやろ。RSC Payloadってやつかね。

まとめ

やはりこういう使い方をするならば、Next.jsはベストな選択肢ではなさそう。

もちろん、動的なサイトの一部を静的にできるっていう意味で、この機能自体は有用やと思うけど。 その場合なら、他の多数のページで使うJSが読み込まれても、まあキャッシュされるし・・みたいな気持ちにはなれる。

追記

JSがほとんどないサイトの場合、無駄なバンドルサイズが気になるので採用できないという結論だった。

が、一転して、たとえばJSが必要なコンポーネントが必ず埋まってるサイトであれば?というのも検証してみた。

その場合に気になってたのはビルド時間だが、そこは事前にコンテンツをディスクに書いておくなりキャッシュするなりちゃんと実装しておけば、3000ページくらいなら15sほどで生成できることがわかった。