Client Component だから動的だと思っていたページが、静的に焼けていました

転職用のポートフォリオとして、個人でWebアプリ「Hubpin」を作っています。
制作の経緯は「Hubpin 開発記録」に時系列でまとめています。
設計と判断は自分で行い、実装も原則自分で書いています。AIには手順書・レビュー・調査を任せています。

前回は use server が何を宣言していたのかという話でした。
今回は、その並びで長く誤解していた記号の話です。

公開ページを静的化する作業をしていて、npm run build の出力をよく見るようになりました。
各ページの先頭にこういう記号が付きます。

┌ ○ /
├ ○ /_not-found
├   /[username]
├   ● /matsutake
├   ● /guest
├ ƒ /dashboard
└ ○ /login

ƒ は Dynamic の印です。
このとき自分は「動的ということはブラウザで描画される、つまり Client Component のことだろう」
と読んでいました。CSR という言葉とも結びついていたと思います。

目次

反例が、自分のビルド出力の中にありました

上の出力をもう一度見て止まりました。

○ /login です。

このページは "use client" を書いたフォームを含んでいます。
数日前に自分で書いたので、間違えようがありません。

Client Component を含むページが、静的に焼けている。
自分の読み方が間違っていることが、自分の出力の中に写っていました。

3つの軸が混ざっていました

分けたらこうなりました。

選択肢
いつHTMLを作るか ビルド時( )/ リクエストのたび(ƒ
どこで作るか サーバー / ブラウザ
Server / Client Component "use client" の有無

ビルド出力が言っているのは、1行目だけでした。

ƒ も、HTMLを作っているのはサーバーです。
違うのは作るタイミングのほうでした。

そして "use client" は「ブラウザでも動く」という意味であって、
「サーバーで描画しない」ではありません。
Client Component もサーバーで初回のHTMLを作ってから、ブラウザへ渡されます。

3つとも同じページについて、それぞれ別に決まります。

では ƒ を決めているのは何か

/dashboardƒ なのは、"use client" があるからではありませんでした。

このページはログイン中のユーザーを確かめるために Supabase のクライアントを作っていて、
その中で cookies() を読んでいます。
誰がアクセスしたかで中身が変わるので、ビルド時にHTMLを作れません。

判定はここだけでした。

リクエストごとに答えが変わるか。

誰が見ても同じHTMLでいいなら焼ける。
見る人によって変わるなら焼けない。それだけの話でした。

/login が焼けるのは、誰が開いても同じフォームが出るからです。
中の "use client" は、焼けるかどうかに関係していません。

なぜ混ざったのか

「Client Component」「CSR」「動的」が、全部同じ方向の言葉に見えたからだと思います。

どれも「ブラウザ寄り」という気配があります。
実際には、"use client" は書ける場所の話で、ƒ は焼けるかどうかの話でした。

知らない単語なら調べます。
知っている単語が並んでいると、そのまま自分の文脈で読んでしまう。

おわりに

この誤解が解けたのは、他人の説明を読んだからではありませんでした。
数日前に自分で書いたコードが、反例としてビルド出力に並んでいたからです。

記号の意味を調べるより、○ /login の1行を見て止まったほうが早かった。
自分の出力の中に答えがあることは、わりとあるのだと思います。

次回は、この静的化を進めた結果、
壊れていないコードで CI が落ちた話を書きます。

目次