定義
アプリの初回進入でネイティブアプリのようにロゴスプラッシュを 初回ペイントから 見せつつ、JavaScript を実行しない検索クローラーの索引にはそのスプラッシュが入らないようにしたいときがある。このために、サーバーレンダー(SSR)マークアップや User-Agent 分岐ではなく、HTML <body> 最上部に インラインブロッキングスクリプト を入れてスプラッシュノードを DOM に直接作る。ハイドレーションが終わるとクライアントが opacity で取り除く。
インラインブロッキングスクリプト は HTML に直接埋め込まれ、パーサーが出会った瞬間に実行される <script> だ。JS を回さないクローラーはこれを実行しないので、スプラッシュはボットが見る HTML には残らない。
なぜ必要か
三つの代替案がそれぞれ塞がれているため、このパターンが必要になる。
- SSR マークアップ を使うとスプラッシュが初回 HTML にそのまま載ってクローラー索引に漏れる(望まないローディングテキストが検索結果に露出)。
- UA ゲート(サーバーでリクエストヘッダーを読んで「ボットならスプラッシュをスキップ」)は、共有レイアウトで
headers()を読んだ瞬間、その下のルートの 静的生成/ISR を強制的に dynamic に変え静的キャッシュをまるごと無力化 する。 - mount 以降にのみ現れるクライアントスプラッシュ は、サーバーで初期状態が空で初回 HTML が空になるため、「ローディング初画面」としては初回ペイントのギャップが生じて不適切だ。
インラインスクリプト注入は静的レンダー/ISR を維持しながら JS 実行時点にのみスプラッシュを描いて三つの問題を同時に避ける。ここで ISR(Incremental Static Regeneration) はページをあらかじめ静的に焼いておいて定期的にのみ更新するキャッシュ方式であり、dynamic 強制 とはリクエストごとにサーバーが新たにレンダーするようになることを意味する。
動作原理
サーバー: 静的/ISR HTML 生成(スプラッシュは SSR で入れない)
→ HTML レスポンスに <body> 最上部のインライン <script> を含む
ブラウザパーサー:
1. <script> を即実行 → スプラッシュノードを body 最上部に prepend(初回ペイント前)
2. 残りの HTML パース + React ハイドレーション
→ React 19 が body/head 上部の予想外ノードを tolerant にスキップ
3. ハイドレーション完了 → クライアントがスプラッシュを opacity で除去
クローラー(JS 未実行): <script> を回さない → スプラッシュノードなし
- 最新の React(19)は body/head 上部に第三者(ブラウザ拡張・スクリプト)が挟み込んだ予想外ノードをハイドレーションで寛容に飛ばす。このおかげで SSR なしで注入しても hydration mismatch が起きない。
- スプラッシュノードは React ツリー外(スクリプトが作ったもの)なので、クライアントが DOM から直接除去しても再調整の衝突がない。
実務での適用
// サーバーレイアウト: 文字列ビルダーが作ったインラインスクリプトを注入
function buildBootSplashScript(brand: string) {
// '<' を Unicode エスケープして </script> の早期終了を遮断
const safe = JSON.stringify(brand).replace(/</g, "\\u003c");
return `(function(){var n=document.createElement('div');
n.className='boot-splash';n.textContent=${safe};
document.body.prepend(n);})();`;
}
export default function AppLayout({ children }) {
return (
<>
<script
dangerouslySetInnerHTML={{ __html: buildBootSplashScript(BRAND) }}
/>
{children}
</>
);
}
.boot-splash {
position: fixed;
inset: 0;
z-index: 110;
background: var(
--bg
); /* body/themeColor と同じトークン → 取り除かれるとき色ジャンプなし */
opacity: 1;
transition: opacity 200ms;
}
@media (prefers-reduced-motion: reduce) {
.boot-splash {
transition: none;
}
}
ハイドレーション後にクライアントが .boot-splash に opacity 0 を与え、transition 終了時に除去する。
トレードオフ
- インラインスクリプト注入: 静的キャッシュ/ISR を守り、ボット隠蔽・初回ペイント表示を両方得る。代わりに文字列でスクリプトを作るためエスケープ・テストに気を遣う必要がある。
- SSR マークアップ: 実装が最も簡単だがボット索引に漏れる。
- UA ゲート: ボット隠蔽はできるが静的キャッシュを壊す。
使ってはいけない場合
- ボット隠蔽が必要ない場合 — 素直に SSR/CSS スプラッシュのほうが単純だ。
- 下位ルートに静的/ISR キャッシュが一切ない純粋な動的アプリ — UA ゲートを使っても失うキャッシュがないので、このパターンの利点が減る。
よくある間違い
- 共有レイアウトで
headers()/cookies()を無意識に使い、下位 ISR ルートを dynamic に壊す。 - インラインスクリプトに入れる文字列に
</script>が混ざりスクリプトが早期終了(エスケープ・textContentで防止、境界値テストで検証)。 - スプラッシュの背景色を body と違うものにして、取り除かれる瞬間に色ジャンプが発生。
関連概念
- ssr-hydration-mismatch — 注入ノードをハイドレーションがどう扱うかの前提
- usesyncexternalstore-snapshot-identity — サーバーで空になる mount-gated クライアント状態
- critical-rendering-path — ブロッキングスクリプトがパース・ペイントに与える影響