12k
All articles

モダンフロントエンドアプリケーションにおけるFOUCの防止

ReactおよびNext.jsアプリのFOUCを、クリティカルCSSインライン化・SSRスタイル抽出・font-display制御・ハイドレーション順序の最適化で防止する。

OpenReplay Team
OpenReplay Team
モダンフロントエンドアプリケーションにおけるFOUCの防止

洗練されたReactやNext.jsアプリケーションを構築し、デプロイした後、ユーザーが慎重に作り込んだUIが表示される前に、スタイルが適用されていないコンテンツが一瞬表示される様子を見て愕然とした経験はありませんか。このFlash of Unstyled Content(FOUC、スタイル未適用コンテンツの一瞬表示)は、ユーザーの信頼を損ない、Core Web Vitalsスコアに悪影響を及ぼす可能性があります。

本ガイドでは、モダンなフロントエンドアーキテクチャにおいてFOUCが発生する理由と、フレームワークに依存しない持続可能な原則を用いてそれを排除する方法について説明します。

重要なポイント

  • FOUCは、ハイドレーションのタイミング、コード分割、またはフォント読み込みの遅延により、ブラウザがスタイルが完全に適用される前にHTMLをレンダリングする際に発生します。
  • クリティカルCSSのインライン化と、サーバーサイド抽出のためのCSS-in-JSの設定は、SSRアプリにおけるスタイルのフラッシュに対する最も効果的な防御策の一つです。
  • 適切なfont-display戦略とフレームワークレベルのフォント最適化(next/fontなど)を使用して、フォント関連のレイアウトシフトを防ぎます。
  • 遅延ロードされるコンポーネントを監査し、コンテンツとスタイル間の競合状態を検出するため、必ず帯域制限された接続でテストを行ってください。

モダンアプリにおけるFlash of Unstyled Contentの原因

FOUCは、ブラウザがスタイルが完全に適用される前にHTMLをレンダリングする際に発生します。従来のサイトでは、これはCSSファイルの読み込みが遅いことを意味していました。モダンなアプリでは、原因はより複雑です。

ハイドレーションとスタイルのフラッシュ

Next.jsのようなサーバーサイドレンダリング(SSR)アプリケーションは、HTMLを即座にブラウザに送信します。ブラウザはこのコンテンツを描画し、その後JavaScriptがページを「ハイドレート」してインタラクティブにします。スタイリングソリューションがハイドレーション中にスタイルを注入する場合(CSS-in-JSライブラリでよく見られる)、ユーザーはJavaScriptが実行されるまでスタイルが適用されていないコンテンツを目にします。

ストリーミングSSRはこれをさらに複雑にします。HTMLチャンクが到着すると、ブラウザはそれらを段階的にレンダリングします。対応するHTMLよりも後に到着したスタイルは、目に見えるフラッシュを引き起こします。

コード分割と動的インポート

コンポーネントを遅延ロードする場合、そのスタイルも一緒にロードされることがよくあります。動的にインポートされたモーダルやサイドバーは、CSSがまだ解析されていないため、最初にマウントされる際にスタイルが適用されていない状態で一瞬表示される可能性があります。

フォント読み込みとFOUC

カスタムフォントは独自のバリエーションを引き起こします:Flash of Unstyled Text(FOUT、スタイル未適用テキストの一瞬表示)です。ブラウザはフォールバックフォントでテキストをレンダリングし、カスタムフォントが読み込まれるとリフローします。これにより、目に見えるテキストのシフトとスタイルの不一致が発生します。

SSRとハイドレーションにおけるFOUCの防止方法

核となる原則は明快です:スタイルは対応するHTMLの前または同時に到着する必要があります。

クリティカルCSSのインライン化

ファーストビューのコンテンツに必要なスタイルを抽出し、ドキュメントの<head>にインライン化します。これにより、ブラウザが何かを描画する前にスタイルを持つことが保証されます。

<head>
  <style>
    /* Critical styles for initial viewport */
    .hero { display: flex; min-height: 100vh; }
    .nav { position: fixed; top: 0; }
  </style>
</head>

Criticalのようなビルド時ツールは、ビルド中にファーストビューのスタイルを生成してインライン化することで、クリティカルCSSの抽出を自動化できます。Next.jsを含む多くのモダンフレームワークも、組み込みのスタイリングソリューションに対してCSS配信を最適化し、最初の描画前に必須のスタイルが利用可能であることを保証します。

決定論的なスタイル注入の確保

CSS-in-JSを使用する場合は、ビルド時にスタイルを抽出するか、SSR中に注入するように設定します。Styled ComponentsやEmotionのようなライブラリは、サーバーサイドのスタイル抽出をサポートしています。これがないと、スタイルはJavaScriptが実行された後にのみ存在します。

// Next.js with Styled Components requires compiler config
// next.config.js
module.exports = {
  compiler: {
    styledComponents: true,
  },
}

レンダリング順序の制御

<head>内のすべてのスクリプトの前にスタイルシートの<link>タグを配置します。head内のCSSファイルはデフォルトでレンダーブロッキングです—これは実際にクリティカルなスタイルにとって望ましいことです。ブラウザはこれらのスタイルが読み込まれるまで描画しません。

非クリティカルなスタイルについては、非同期で読み込みます:

<link rel="preload" href="/non-critical.css" as="style" onload="this.rel='stylesheet'">
<noscript><link rel="stylesheet" href="/non-critical.css"></noscript>

<noscript>フォールバックに注意してください:これがないと、JavaScriptが無効になっているユーザーは、onloadハンドラが発火しないため、スタイルシートを受け取ることができません。

フォント読み込みによるFOUCの排除

フォント関連のフラッシュには、font-displayプロパティの明示的な管理が必要です。

font-display戦略の選択

  • font-display: swapは、フォールバックテキストを即座に表示し、フォントが読み込まれたら切り替えます(リフローを引き起こす可能性があります)
  • font-display: optionalは、すでにキャッシュされている場合のみカスタムフォントを使用します(フラッシュは最小限ですが、初回訪問時にフォントが表示されない可能性があります)
  • font-display: fallbackは、短いブロック期間で両者のバランスを取ります

適切な選択は優先事項によって異なります。swapは即座の可読性を優先し、fallbackoptionalは、より厳格な読み込み動作を犠牲にしてレイアウトシフトを減らすことができます。

フレームワークのフォント最適化の使用

Next.jsのnext/fontは、フォント読み込みを自動的に処理し、フォント宣言をインライン化し、外部ネットワークリクエストを排除します:

import { Inter } from 'next/font/google'

const inter = Inter({ subsets: ['latin'] })

export default function RootLayout({ children }) {
  return (
    <html lang="en" className={inter.className}>
      <body>{children}</body>
    </html>
  )
}

このアプローチは、フォントファイルをセルフホスティングし、ビルド時に@font-face宣言をインライン化することで、Google Fontsへの外部リクエストの必要性を排除し、フォント関連のフラッシュを防ぐのに役立ちます。

View Transitionsにおけるフラッシュの防止

View Transitions APIは、スムーズなページ遷移を可能にしますが、誤用するとスタイルが適用されていない状態を露呈する可能性があります。

トランジションがスタイルが読み込まれる前の「古い」状態や、ハイドレーションが完了する前の「新しい」状態をキャプチャすると、ユーザーは中間的なスタイル未適用のフレームを目にします。コンテンツとスタイルの両方が準備できた後にのみトランジションが開始されるようにしてください:

// Wait for styles before starting transition
document.startViewTransition(async () => {
  await ensureStylesLoaded() // pseudo-code
  updateDOM()
})

ブラウザサポートは拡大していますが、エンジン間でまだ異なるため、互換性を確認し、必要に応じて適切なフォールバックを提供してください。

FOUCを排除するための実践的チェックリスト

  1. ファーストビューコンテンツのクリティカルCSSをインライン化する
  2. CSS-in-JSをサーバーサイド抽出用に設定する
  3. リソースを正しく順序付けする:<head>内でCSSをJavaScriptの前に配置
  4. UXの優先事項に基づいて適切なfont-display戦略を選択する
  5. 外部フォントリンクの代わりにフレームワークのフォント最適化を使用する
  6. 帯域制限された接続でテストして競合状態を検出する
  7. 遅延ロードされるコンポーネントをスタイルのタイミング問題について監査する

まとめ

モダンなフロントエンドアプリにおけるFOUCの防止は、一つの原則に集約されます:スタイルがコンテンツと同時またはその前に到着することを保証することです。ハイドレーションのタイミング、コード分割されたコンポーネント、またはフォント読み込みに対処する場合でも、解決策は常に操作の順序を制御することです。

レンダリングパイプラインを監査し、クリティカルなものをインライン化し、非必須のスタイルはブロッキングせずに読み込むようにしてください。ユーザー、そしてLighthouseスコアがそれに感謝するでしょう。

よくある質問

FOUCはSEOやCore Web Vitalsスコアに影響しますか?

FOUCは、Cumulative Layout Shift(CLS)とLargest Contentful Paint(LCP)の両方に影響を与える可能性があり、これらはGoogleがランキングに使用するCore Web Vitals指標です。スタイルが読み込まれる際にスタイル未適用のコンテンツがリフローすると、CLSが増加する可能性があり、スタイルが適用されたファーストビューコンテンツのレンダリングが遅延すると、LCPが上昇する可能性があります。したがって、FOUCを修正することで両方の指標を改善できます。

CSS ModulesはNext.jsでFOUCを引き起こす可能性がありますか?

Next.jsのCSS Modulesは、スタイルが抽出されてページと一緒に配信されるため、FOUCのリスクを軽減するように設計されています。ただし、ハイドレーションのタイミング、ストリーミング、またはクラス名を条件付きで適用するクライアント専用ロジックは、依然として短いフラッシュを引き起こす可能性があります。リスクを最小限に抑えるため、サーバー上で初期クラス割り当てを決定論的に保ってください。

開発中にFOUCをテストするにはどうすればよいですか?

Chrome DevToolsを使用してネットワークをSlow 3Gにスロットルし、キャッシュを無効にします。これにより、スタイルシートとフォントがゆっくり読み込まれる状況をシミュレートし、FOUCを可視化できます。また、パフォーマンストレースを記録し、個々のフレームでスタイル未適用のペイントイベントを検査することもできます。シークレットモードでテストすることで、キャッシュされたフォントやスタイルが問題を隠さないようにします。

FOUCを避けるには、CSS-in-JSとCSS Modulesのどちらを使用する方が良いですか?

静的CSSアプローチは、スタイルがビルド時に生成され、標準的なスタイルシートとして提供されるため、通常より予測可能です。CSS-in-JSライブラリも同様に信頼性がありますが、通常、ランタイムスタイル注入を避けるために明示的なサーバーサイド抽出が必要です。より安全な選択は、最初の描画前にスタイルが利用可能であることを保証するどちらのアプローチでもあります。

DevTools for the frontend

Gain Debugging Superpowers

Unleash the power of session replay to reproduce bugs, track slowdowns and uncover frustrations in your app. Get complete visibility into your frontend with OpenReplay — the most advanced open-source session replay tool for developers.

Star on GitHub12k

We use cookies to improve your experience. By using our site, you accept cookies.