アトミック・レンダリング:Lighthouse最適化が露出させるUIの時間的不整合と、Edge時代の可視化制御設計のサムネイル

はじめに

Lighthouseで100点を達成した直後、サムネイル画像のALTテキストがリロードのたびに一瞬表示されるようになりました。Lazy Load最適化によって「未確定な時間」が可視化されたことが原因です。「表示可能 = 資源確定 ∧ 寸法確定 ∧ レイアウト安定」のアトミック・レンダリング原則で、UIのバイナリステートを強制できます。

UIの破綻は遅延ではなく、時間的不整合の可視化である

AIに指示を出してLighthouseのスコアを改善し、ついにオール100点を達成した。
しかし、ページをリロードするたびに一瞬だけ画像のALTテキストが無防備に表示されたり、レイアウトがカクついたりするようになった。
これはスコアという「数字」の問題ではない。最適化によって時間の分解能が上がり、今まで隠蔽されていた「未確定な時間」が観測可能になってしまったのだ。

この記事が不要な人

  • 完全に静的なサイトを構築しており、画像のリソースがビルド時に確定している人
  • CSR(クライアントサイドレンダリング)のみで描画順序が常に固定されている人
  • 視覚的な一瞬のチラつきを、UX上の些細な問題として許容できる人

もし一つでも当てはまらないなら、読み進める価値があるかもしれません。

本当の敵は「時間の分解能」

問題の発生条件は明確です。
Edge配信 + SSR + Lazy + Hydration遅延 + 非同期リソース

この組み合わせでのみ、UIの「途中状態」が顕在化します。
分散実行環境では、応答(HTML)と資源(画像・JS)が別々の時間軸で届きます。ここで初めて、UIの「未確定相」が観測されるようになります。

アトミック・レンダリング:観測の制御

この時間的不整合に対抗する手段が「アトミック・レンダリング」です。これは思想ではなく、以下の3条件を満たすための設計制約です。

表示可能 = 資源確定 ∧ 寸法確定 ∧ レイアウト安定

UIは「0(無)」か「1(完全)」しかあってはならない。中途半端な0.5の状態(ALTテキストやプレースホルダー)を許さない。
この「バイナリ・ステート」の原則を、UIコンポーネントの実装に強制します。

このブログもそうでした

私もLighthouseのスコアを追うあまり、AIが生成するコードを鵜呑みにしていました。しかし、AIは最適化を行いますが、時間の整合性までは保証しません。
この記事では、人間がUIの「時間」に責任を持つための設計思想、「アトミック・レンダリング」の具体的な実装を共有します。

📝 概要

Lighthouseのスコア改善、特にLCP(Largest Contentful Paint)やCLS(Cumulative Layout Shift)の最適化は、現代のWeb開発において不可欠です。しかし、Lazy Load(遅延読み込み)の導入やSSR(サーバーサイドレンダリング)との組み合わせは、皮肉にもUIの体感品質を下げる副作用を生むことがあります。

本記事では、この問題の本質を「時間的不整合の可視化」と定義し、Reactにおける堅牢な画像表示コンポーネントの設計思想、「アトミック・レンダリング」について解説します。

発生環境

  • 【フレームワーク】 Remix v2
  • 【ホスティング】 Cloudflare Pages
  • 【ビルドツール】 Vite
  • 【CSS】 Tailwind CSS / CSS Modules

⚠️ 問題の発見と症状

Lighthouseのパフォーマンススコアを改善するために、AIエージェントに「画像コンポーネントの最適化」を依頼しました。AIは loading="lazy"decoding="async" の追加、そして適切な srcset の設定を完璧に行いました。

症状

  • ページ読み込みの瞬間、画像が表示されるべき場所に一瞬だけ「ALTテキスト」が表示される。
  • SSRで配信されたHTMLがクライアントでハイドレーションされるまでの間、画像の枠だけが確保され、中身が「空」の状態でユーザーに晒される。
  • 画像の読み込みに失敗した場合、壊れた画像アイコンが表示され、レイアウトが崩れたままになる。

スコアは確かに向上しましたが、視覚的な一貫性が失われ、プロダクトの信頼性が損なわれているように感じられました。

なぜNext.js全盛期(Node時代)には目立たなかったか

答えは単純です。実行環境の構造的差異にあります。

  • 単一実行環境(Node.js等): 応答時間 ≈ 資源到達時間 → 未確定時間が短く観測されない
  • 分散実行環境(Edge): 応答時間 << 資源到達時間 → 未確定時間が露出

Cloudflareのような分散実行環境では、HTMLの応答速度が爆発的に向上した反面、静的リソースの取得とのタイムラグ(時間差)が観測されやすくなりました。
つまりこれはAI時代の問題というより、分散実行時代の問題です。AIによる最適化は、この露出を加速させたに過ぎません。

🔍 調査と試行錯誤のプロセス

仮説1: プレースホルダー(ダミー画像)を表示すれば解決するのではないか?

【試したこと】
画像の読み込み中にスケルトンスクリーンや、ぼかした低解像度の画像(LQIP)を表示するようにしました。

【結果】
失敗。

【分かったこと】
根本的な解決になっていない。プレースホルダーは別の「中間状態」を生み出すだけで、「0か1か」の原則に反します。問題は「何を見せるか」ではなく、「不完全な状態を見せない」ことです。

仮説2: SSRでの初期描画を完全に抑制する

【試したこと】
useEffect が実行されるまで(クライアントサイドであることが確定するまで)画像を表示しないように変更しました。

【結果】
失敗。

【分かったこと】
LighthouseのLCPスコアが大幅に悪化しました。SSRの利点である「HTMLに画像情報を載せてブラウザのプリローダーに気づかせる」というメリットを殺してしまいました。プラットフォームの利点を捨てるのは、正しい解決策ではありません。

💡 根本原因の特定

根本的な原因は、 Lighthouseによる順序最適化が、設計の穴を露出させたこと にありました。

【原因】
Lighthouseは「順序を最適化」します。対して、設計は「可視化条件を定義」しなければなりません。
順序最適化が進むと、これまで隠蔽されていた「リソース取得待ち」や「ハイドレーション待ち」の時間が表面化します。

つまり、Lighthouseは原因ではなく、設計の不備を暴く「露出装置」だったのです。

【なぜこの問題が起きたか】
AIはスコア(順序)を最適化しますが、可視化条件(制約)までは考慮しません。
人間が「表示可能 = 資源確定 ∧ 寸法確定 ∧ レイアウト安定」という強い設計制約を課さない限り、システムは最も効率的で、最も不安定な状態をユーザーに見せ続けます。

🔧 解決策:アトミック・レンダリングの実装

UIの状態が「完全(loaded)」にコミットされるまで、その存在を一切外部に露出させない「アトミック・レンダリング」を実装します。

// app/components/blog/posts/PostCard.tsx (抜粋)

const PostCard = ({ thumbnailUrl, category, title }) => {
  // UIの状態を 'loading' | 'loaded' | 'error' で管理
  const [status, setStatus] = useState('loading');

  // 失敗(error)した場合、このコンポーネントは「存在しなかった」ことになる(ロールバック)
  if (status === 'error') {
    return null;
  }

  return (
    <div className="thumbnail-container">
      <img
        src={thumbnailUrl}
        alt={title}
        loading="lazy"
        decoding="async"
        onLoad={() => setStatus('loaded')}
        onError={() => setStatus('error')}
        // 状態が 'loaded' にコミットされるまで、UIは完全に不可視
        style={{
          opacity: status === 'loaded' ? 1 : 0,
          transition: 'opacity 0.2s ease-in-out',
        }}
      />
    </div>
  );
};

変更のポイント

  • 【バイナリ・ステート(0 or 1)の強制】 opacity を使って、UIの状態を「完全に見える(1)」か「完全に見えない(0)」のどちらかに強制します。これは「資源確定」まで可視化をブロックする制御です。なお、opacity: 0 でもレイアウトは占有されるため、「寸法確定」の条件とは整合します。
  • 【UIトランザクションのロールバック】 画像の読み込みに失敗した場合、コンポーネントは null を返します。これはUIのトランザクションが失敗し、描画自体を「ロールバック」したことを意味します。中途半端な失敗状態(壊れた画像アイコンなど)をユーザーに見せません。
  • 【状態駆動の設計】 <style> タグを注入するようなハックではなく、Reactの state に基づいてクリーンにUIを制御します。

🎓 学んだこと・まとめ

技術的な学び

  • 【UIは見た目ではなく、観測制御の問題である】 人間が扱っているのはピクセルではなく「時間」です。分散環境では、いつ、何を見せるかという「観測の制御」こそがエンジニアリングの本質になります。
  • 【レンダリング制御の重要性】 SSRやLazy Loadを捨てるのではなく、それらが引き起こす時間的不整合を state で制御する。これがEdge時代の正しいレンダリング哲学です。

今後のベストプラクティス

  • 【アトミック・レンダリングの原則】 複雑なスケルトンスクリーンを実装する前に、まず「不完全な状態を見せない」というアトミック・レンダリングの原則を適用できないか検討すべきです。多くの場合、これが最も堅牢でシンプルな解決策となります。
  • 【環境の揺らぎを前提にする】 Edge環境やモバイルネットワークでは、SSRとハイドレーションの「タイムラグ」は常に発生します。このラグを設計対象に含めることが、モダンなWeb開発の必須条件です。

🔗 関連リソース

同じような問題で困っている方の参考になれば幸いです!

この「UIを時間として管理する」という問題は、Lighthouseがスコアを測定するためのネットワーク条件に起因する構造的な制約とも深く関わっています。Remix + ViteスタックがPSI測定で直面する上限値の分析を読むと、「なぜEdge配信でも遅延が生まれるのか」を数値ベースで理解できます。また、サムネイル画像をメインリポジトリから切り離して外部管理するリポジトリ分離設計によるAIコンテキスト軽量化という方向性も、画像UI問題を根本から再定義する視点として参照してください。

あなたのリポジトリへの適用

どちらを採用しますか?(1つ選択)

SAFETY|安全性を高める

サムネイルのアトミック・レンダリング原則が現在の実装で保証されているか確認する

調査:

  1. サムネイル表示の「資源確定 ∧ 寸法確定 ∧ レイアウト安定」の3条件が全て満たされているか確認せよ
  2. Lazy Load最適化後にALTテキストが一瞬表示される問題が再発していないか確認せよ
  3. 新しく追加した画像コンポーネントでも同じ原則が適用されているか確認せよ

この内容をClaude Codeに貼り付けて実行します

PERFORMANCE|パフォーマンス改善

UIのバイナリステート(表示/非表示)が一貫して制御されているか確認する

調査:

  1. 「未確定状態」(画像読み込み中・データ取得中)のUI表示ロジックを特定せよ
  2. バイナリステート(完全に表示 or 完全に非表示)が守られていないコンポーネントを確認せよ
  3. CLS(Cumulative Layout Shift)を引き起こす中間状態の表示がないか確認せよ

この内容をClaude Codeに貼り付けて実行します

外部コードのローカル実行にはリスクがあります。ブラウザ環境での実行を推奨します。