ブラウザに何が届くのか
Server Component のコードはブラウザに届きませんが、Client Component へ渡した props は表示していなくても全部届きます。
#送られるのは「結果」であって「コード」ではない
Server Component を書いても、そのコードはブラウザに届きません。届くのは、サーバーで描画した結果です。
具体的には2つ送られます。
HTML 最初の表示に使う
RSC ペイロード React が画面を組み立て直すための、木構造のデータ
RSC ペイロードには、コンポーネントの構造と、境界を越えた props の値が入っています。開発者ツールのネットワークタブや、ページのソースから読めます。
#境界を越えた props は、表示していなくても届く
ここが実務でいちばん重要です。
// app/admin/page.tsx(Server Component)
export default function Page() {
const key = process.env.ADMIN_KEY // NEXT_PUBLIC_ は付いていない
return <Table apiKey={key} /> // ← 越えた
}
このコードをビルドすると、成果物の中に ADMIN_KEY の値がそのまま入ります。 画面に表示していなくても入ります。
このサイトでは、これを説明で済ませていません。この問題は、実際に Next.js 16.3.3 でビルドして、成果物に値が含まれることを毎回確かめています。
#NEXT_PUBLIC_ が守るもの、守らないもの
よくある誤解が「NEXT_PUBLIC_ を付けていないから安全」です。
| 何が起きるか | |
|---|---|
NEXT_PUBLIC_ を付ける | その環境変数がクライアント側のバンドルに埋め込まれる |
| 付けない | 埋め込まれない |
| 自分で props として渡す | 付けていなくても届く |
NEXT_PUBLIC_ が制御しているのは「変数の埋め込み」だけです。自分の手で境界を越えさせた値は、その対象外です。仕組みが守ってくれるのはここまでで、あとは書いた人の責任になります。
#使っていないプロパティも届く
受け取る側が読まなくても、渡したオブジェクトの中身は全部運ばれます。
const user = await getUser() // { id, name, passwordHash, ... }
return <Profile user={user} /> // Profile は name しか使わない
これでも passwordHash は届きます。型で { name: string } と宣言しても関係ありません。型は実行時には消えているので、何も削りません。
ORM が返した行をそのまま渡すのが危ない、というのはこの意味です。
#越えなければ残らない
逆に、境界を越えなければ値は残りません。
export default async function Page() {
const key = process.env.ADMIN_KEY
const rows = await load(key) // サーバーの中で使い切る
return <div>{rows.length}</div> // 渡すのは件数だけ
}
これは実際に確かめて、成果物に値が含まれないことを確認しています。
安全なのは「使ったこと」ではなく「渡さなかったこと」です。
#実務での書き方
1. 秘密はサーバーの中で使い切る
API キーが要るなら、その呼び出しごとサーバー側に置き、外へ出すのは結果だけにします。
2. 渡す形を明示的に組み立てる
const row = await db.user.findFirst()
const user = {
id: row.id,
name: row.name,
}
return <Profile user={user} />
面倒に見えますが、これ1つでシリアライズのエラーと情報漏れの両方が防げます。
3. レビューで見る場所を1つに絞る
「Client Component に渡している props に、見せてはいけないものが無いか」だけを見ればよくなります。境界がはっきりしているほど、確認は速く終わります。
#確認のしかた
自分のページで確かめたいときは、ビルドした成果物を検索するのが確実です。
next build
grep -r "確かめたい文字列" .next/server/app
ブラウザで確かめるなら、ページのソースを表示して同じ文字列を探します。開発者ツールを開かない利用者には見えませんが、見ようと思えば誰でも見られるという前提で設計してください。