自分のサービスを開いたら、一瞬だけスタイルの当たっていない画面が出ました。原因は CSS の遅延ではなく、SEO のために手書きしていた静的 HTML を、React が起動と同時に捨てて描き直していたことでした。対症療法を却下されて、初期 HTML を React の実出力そのものに置き換えるまでの記録です。剥がしてみたら、隠れていた UI の欠陥まで出てきました。
React の SPA が配信する HTML は、素のままだと中身が空です。JavaScript を実行しないクローラーには読めないので、ビルド時にクローラー向けの HTML を作って #root に入れておく。これがプリレンダーです。ところが React を createRoot で起動すると、その中身をマウント時に全部捨てて描き直します。捨てられるまでの数百ミリ秒、手書きの HTML がそのままユーザーに見えていました。
根治は 1 つだけでした。初期 HTML を React の実出力そのものにして、hydrateRoot で引き継ぐ。同じものが置いてあるなら捨てる必要がないので、置き換えが起きません。ただしこれには制約が付いてきます。サーバー出力とブラウザの初回描画が 1 文字でも違うと React は描き直すので、「HTML には載せず JS 起動後にだけ出す」ができなくなります。
手書きの別 HTML を createRoot が捨てる旧方式と、React の SSR 出力を hydrateRoot が引き継ぐ新方式の比較。
いちばんの収穫は副産物でした。手書きの静的 HTML を剥がした瞬間、ビルド時の検査が次々に落ち始め、その理由がどれもアプリ側の実際の欠陥だったことです。学校詳細ページに <main> が無い、<h1> が無い、内部リンクが canonical と違う末尾スラッシュ無しになっている。手書き HTML が正しい形を出していたので、UI 側の不備が表に出ていませんでした。