このサイトの昼夜ムード切替では、リロード時に一瞬デフォルトの状態が見えてしまう「チラつき」を、<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つだけ確定させる」という最小の役割に絞ることで、このパターンはムード切替以外の場面にも安全に持ち出せると考えています。