このサイトの昼夜ムード切替では、リロード時に一瞬デフォルトの状態が見えてしまう「チラつき」を、<head> 内の同期インラインスクリプトで防いでいます。この仕組みは、ムード切替専用のテクニックではなく、もっと一般化できるパターンだと思っています。

「先読みが必要な状態」を見分ける

var m = localStorage.getItem("mode") || "night";
document.documentElement.setAttribute("data-mode", m);

このパターンが効くのは、「ユーザーの選択が視覚的な初期状態に直結し、かつCSSがその状態を属性やクラスで参照している」場合に限られます。ムード以外でも、たとえば文字サイズの好み、モーション演出のON/OFF、表示言語の初期値など、同じ形の分岐候補は複数あり得ます。逆に、選択がJSでのDOM生成そのものに関わる場合(例: 表示するコンポーネントの出し分け)は、この3行では対処できず、もっと大掛かりなハイドレーションの仕組みが必要になります。

Astroでは is:inline を忘れない

Astroのスクリプトはデフォルトでバンドル・モジュール化されますが、それだと実行タイミングが遅れてしまい、FOUC対策の意味がなくなります。is:inline を付けて、素の <script> タグとしてそのまま出力させる必要があります。

<script is:inline>
  (function () {
    try {
      var m = localStorage.getItem("portfolio-mode") || "night";
      document.documentElement.setAttribute("data-mode", m);
    } catch (e) {}
  })();
</script>

スクリプトは小さく保つ、が絶対条件

このスクリプトは <head> の中でパースをブロックします。つまり中身が重いと、それだけページの初期表示が遅れます。ここに複雑なロジックを書きたくなったら、それは「先読みすべき状態」の設計そのものを見直すべきサインだと考えています。あくまで「属性を1つ立てるだけ」の最小限にとどめ、実際の見た目の切り替えはCSS側にすべて委ねる——という役割分担を崩さないことが、このパターンを安全に使い回すコツです。

try/catch で localStorage へのアクセスを囲っているのも地味に重要です。プライベートブラウジングなど一部の環境では localStorage へのアクセスが例外を投げることがあり、ここで落ちるとページ全体がレンダリングされなくなってしまいます。

「初回描画より前に、属性を1つだけ確定させる」という最小の役割に絞ることで、このパターンはムード切替以外の場面にも安全に持ち出せると考えています。