T-CREATOR

Emotion で FOUC が出る原因と解決策:挿入順序/SSR 抽出/プリロードの総点検

Emotion で FOUC が出る原因と解決策:挿入順序/SSR 抽出/プリロードの総点検

React や Next.js で CSS-in-JS ライブラリの Emotion を導入したとき、ページがロードされた瞬間にスタイルが適用されていない状態が一瞬表示される——いわゆる FOUC(Flash of Unstyled Content) に悩まされた経験はありませんか。

せっかく美しいデザインを実装しても、ユーザーが最初に目にするのがスタイルの崩れた画面では、プロフェッショナルな印象を損ねてしまいます。実は Emotion で FOUC が発生する原因は、スタイルの挿入順序や SSR 時の抽出処理、そしてプリロード設定など、複数の要因が複雑に絡み合っているのです。

本記事では、Emotion における FOUC の根本原因 を整理し、挿入順序の制御SSR でのスタイル抽出プリロード最適化 という 3 つの観点から、実践的な解決策を詳しくご紹介します。各セクションでは具体的なコード例と図解を交え、初心者の方でも理解しやすいように段階的に解説していきますね。

背景

FOUC とは何か

FOUC(Flash of Unstyled Content)とは、Web ページが読み込まれる際に、CSS が適用される前の素のコンテンツが一瞬表示される現象を指します。

訪問者がページを開いた瞬間、レイアウトが崩れたテキストやボタンが表示され、その後すぐに正しいデザインに切り替わるため、ユーザー体験を大きく損なう要因となるでしょう。

Emotion の基本的な仕組み

Emotion は、JavaScript の中でスタイルを定義できる CSS-in-JS ライブラリです。

従来の CSS ファイルとは異なり、コンポーネントと密接に結びついたスタイル管理が可能になります。Emotion では、実行時に JavaScript がスタイルを生成し、<style> タグとして HTML の <head> 内に動的に挿入する仕組みを採用しています。

mermaidflowchart LR
  comp["React<br/>コンポーネント"] -->|スタイル定義| emotion["Emotion<br/>ライブラリ"]
  emotion -->|CSS 生成| styleNode["&lt;style&gt; タグ<br/>を挿入"]
  styleNode -->|適用| dom["ブラウザ<br/>DOM"]

図で理解できる要点:

  • コンポーネント内でスタイルを定義
  • Emotion が実行時に CSS を生成し、<style> タグとして DOM に挿入
  • ブラウザが最終的にスタイルを適用

SSR(Server-Side Rendering)における課題

Next.js のような SSR フレームワークでは、サーバー側で HTML を事前にレンダリングしてクライアントに送信します。

しかし、Emotion が生成するスタイルは本来 JavaScript の実行時に動的に作られるため、サーバー側で生成された HTML には スタイル情報が含まれていない ことがあります。その結果、ブラウザが HTML を受け取った時点ではスタイルが適用されておらず、JavaScript が読み込まれて初めてスタイルが注入されるため、FOUC が発生するのです。

課題

課題 1:スタイルの挿入タイミングのズレ

SSR 環境では、サーバーで生成された HTML が先にブラウザに送られ、その後クライアント側で JavaScript が実行されます。

Emotion のスタイルはクライアント側の JavaScript で生成・挿入されるため、HTML の表示とスタイルの適用に タイムラグ が生じます。この遅延が、ユーザーに一瞬スタイルが適用されていない画面を見せてしまう原因です。

以下の図は、SSR 環境における HTML とスタイルの読み込みフローを示しています。

mermaidsequenceDiagram
  participant Browser as ブラウザ
  participant Server as サーバー
  participant JS as JavaScript

  Browser->>Server: ページリクエスト
  Server->>Browser: HTML 返却<br/>(スタイルなし)
  Browser->>Browser: HTML 表示<br/>(FOUC 発生)
  Browser->>JS: JS ダウンロード
  JS->>Browser: スタイル挿入
  Browser->>Browser: 正しい表示

図で理解できる要点:

  • サーバーから返却される HTML にはスタイル情報が含まれない
  • ブラウザが HTML を表示した瞬間に FOUC が発生
  • JavaScript がダウンロード・実行された後、ようやくスタイルが適用される

課題 2:スタイルの挿入順序が不確定

Emotion は複数のスタイルを動的に生成しますが、その挿入順序が意図しない形になることがあります。

特に、コンポーネントの読み込み順序や条件分岐によってスタイルの挿入タイミングが変わると、CSS の詳細度や優先順位 が崩れ、意図しないスタイルが適用される可能性があるのです。この問題は、開発中に気づきにくく、本番環境で突然発生することもあります。

課題 3:Critical CSS の欠如

通常の CSS 配信では、初回表示に必要な最小限のスタイル(Critical CSS)を <head> 内にインライン展開し、残りのスタイルを非同期で読み込む手法が推奨されます。

しかし、Emotion はデフォルトではこの仕組みを持たないため、すべてのスタイルが JavaScript の実行後に挿入されることになります。その結果、初回表示のパフォーマンスが低下し、FOUC のリスクが高まるでしょう。

課題 4:プリロード設定の不備

HTML の <link rel="preload"> を使えば、ブラウザに重要なリソースを優先的に読み込ませることができます。

しかし、Emotion が生成するスタイルは動的であるため、プリロード対象として明示しにくいのが現状です。この設定不足により、スタイルの読み込みが遅延し、FOUC の発生確率が上がってしまいます。

解決策

解決策 1:createEmotionCache でスタイル挿入順序を制御

Emotion では、createEmotionCache を使って専用のキャッシュを作成し、スタイルの挿入位置や優先順位を明示的にコントロールできます。

この設定により、サーバー側とクライアント側で一貫したスタイル挿入順序を保証し、FOUC を防ぐことが可能です。

以下のコードは、Emotion のキャッシュ作成関数を定義する例です。

typescript// lib/createEmotionCache.ts
import createCache from '@emotion/cache';

/**
 * Emotion のスタイルキャッシュを作成
 * prepend: true にすることで、スタイルを <head> の先頭に挿入
 */

上記の関数は、Emotion のスタイルを <head>先頭に挿入 する設定を行います。

typescriptexport function createEmotionCache() {
  return createCache({
    key: 'css', // キャッシュのキー名
    prepend: true, // スタイルを <head> の先頭に挿入
  });
}

prepend: true により、他の CSS(例えば外部スタイルシートや MUI の CSS)よりも にスタイルが挿入されるため、詳細度の競合を回避できます。

次に、Next.js の _app.tsx でこのキャッシュを利用する設定を行います。

typescript// pages/_app.tsx
import type { AppProps } from 'next/app';
import { CacheProvider, EmotionCache } from '@emotion/react';
import { createEmotionCache } from '../lib/createEmotionCache';

/**
 * クライアント側で使用するデフォルトキャッシュ
 */
const clientSideEmotionCache = createEmotionCache();

上記では、クライアント側で共通して使うキャッシュインスタンスを作成しています。

typescriptinterface MyAppProps extends AppProps {
  emotionCache?: EmotionCache; // サーバー側から渡されるキャッシュ(任意)
}

/**
 * Next.js のカスタム App コンポーネント
 * emotionCache をプロパティで受け取り、CacheProvider に渡す
 */
function MyApp({
  Component,
  pageProps,
  emotionCache = clientSideEmotionCache,
}: MyAppProps) {
  return (
    <CacheProvider value={emotionCache}>
      <Component {...pageProps} />
    </CacheProvider>
  );
}

export default MyApp;

CacheProvider でアプリ全体をラップすることで、すべてのコンポーネントが同じキャッシュを参照し、スタイルの挿入順序が一貫します。

解決策 2:SSR 時にスタイルを抽出して HTML に埋め込む

Next.js の _document.tsx を使えば、サーバーサイドでレンダリングされた HTML に Emotion のスタイルを直接埋め込むことができます。

この処理により、ブラウザが最初に受け取る HTML に すでにスタイルが含まれている 状態になり、FOUC を完全に防げるでしょう。

以下は、_document.tsx でスタイルを抽出・挿入する設定です。

typescript// pages/_document.tsx
import Document, {
  Html,
  Head,
  Main,
  NextScript,
  DocumentContext,
  DocumentInitialProps,
} from 'next/document';
import createEmotionServer from '@emotion/server/create-instance';
import { createEmotionCache } from '../lib/createEmotionCache';

/**
 * Next.js のカスタム Document
 * SSR 時にサーバー側で Emotion スタイルを抽出し、HTML に埋め込む
 */

このクラスでは、getInitialProps メソッドを使ってサーバー側の処理をカスタマイズします。

typescriptclass MyDocument extends Document {
  static async getInitialProps(
    ctx: DocumentContext
  ): Promise<DocumentInitialProps> {
    // サーバー専用の Emotion キャッシュを作成
    const cache = createEmotionCache();
    const { extractCriticalToChunks } = createEmotionServer(cache);

    const originalRenderPage = ctx.renderPage;

createEmotionServer は、サーバーサイドでスタイルを抽出するためのユーティリティを提供します。

typescript    /**
     * renderPage をオーバーライドし、
     * Emotion の CacheProvider でラップされた状態でレンダリング
     */
    ctx.renderPage = () =>
      originalRenderPage({
        enhanceApp: (App) => (props) =>
          (
            <CacheProvider value={cache}>
              <App {...props} />
            </CacheProvider>
          ),
      });

    // 通常の Document の初期データを取得
    const initialProps = await Document.getInitialProps(ctx);

enhanceApp を使うことで、すべてのコンポーネントが同じキャッシュを参照する状態でレンダリングされます。

typescript    /**
     * レンダリング済みの HTML からスタイルチャンクを抽出
     */
    const emotionStyles = extractCriticalToChunks(initialProps.html);
    const emotionStyleTags = emotionStyles.styles.map((style) => (
      <style
        data-emotion={`${style.key} ${style.ids.join(' ')}`}
        key={style.key}
        dangerouslySetInnerHTML={{ __html: style.css }}
      />
    ));

extractCriticalToChunks により、レンダリングされた HTML に必要なスタイルだけを抽出します。

typescript    return {
      ...initialProps,
      // 抽出したスタイルタグを初期データに追加
      styles: [...emotionStyleTags, ...initialProps.styles],
    };
  }

抽出したスタイルを styles プロパティに含めることで、<Head> 内に自動的に挿入されます。

typescript  render() {
    return (
      <Html lang="ja">
        <Head />
        <body>
          <Main />
          <NextScript />
        </body>
      </Html>
    );
  }
}

export default MyDocument;

render メソッドでは、通常通り HTML 構造を定義するだけで、スタイルは自動的に <Head> 内に挿入されます。

解決策 3:スタイルの優先順序を明示する

Emotion と他の CSS ライブラリ(例:MUI、Tailwind CSS)を併用する場合、スタイルの適用順序が競合する可能性があります。

insertionPoint オプションを使えば、Emotion のスタイルを挿入する 基準位置 を HTML 内に明示でき、詳細度の問題を回避できます。

以下のコードでは、<head> 内に挿入基準点を設置します。

typescript// lib/createEmotionCache.ts(拡張版)
import createCache from '@emotion/cache';

/**
 * insertionPoint を指定して、
 * Emotion スタイルの挿入位置を制御
 */
export function createEmotionCache() {
  // ブラウザ環境の場合のみ insertionPoint を参照
  const isBrowser = typeof document !== 'undefined';

サーバーサイドでは document が存在しないため、条件分岐が必要です。

typescript  return createCache({
    key: 'css',
    prepend: true,
    insertionPoint: isBrowser
      ? document.querySelector<HTMLMetaElement>('#emotion-insertion-point')!
      : undefined,
  });
}

insertionPoint に指定した要素の 直後 にスタイルが挿入されるため、他の CSS との順序を制御できます。

次に、_document.tsx で挿入基準点となる <meta> タグを設置します。

typescript// pages/_document.tsx(一部抜粋)
class MyDocument extends Document {
  render() {
    return (
      <Html lang="ja">
        <Head>
          {/* Emotion スタイルの挿入基準点 */}
          <meta name="emotion-insertion-point" content="" />
        </Head>
        <body>
          <Main />
          <NextScript />
        </body>
      </Html>
    );
  }
}

この <meta> タグの直後に Emotion のスタイルが挿入されるため、他の CSS との順序が明確になります。

解決策 4:Critical CSS をプリロード

初回表示に必要な最小限のスタイルを <link rel="preload"> でプリロードすることで、ブラウザに優先的に読み込ませることができます。

Emotion では静的なスタイルファイルとして抽出するのが難しいため、代わりに SSR で抽出したインラインスタイル をそのまま HTML に埋め込む手法が有効です。

以下は、_document.tsx でプリロード用のメタタグを追加する例です。

typescript// pages/_document.tsx(一部抜粋)
class MyDocument extends Document {
  render() {
    return (
      <Html lang="ja">
        <Head>
          {/* フォントやアイコンのプリロード例 */}
          <link
            rel="preload"
            href="/fonts/your-font.woff2"
            as="font"
            type="font/woff2"
            crossOrigin="anonymous"
          />

          <meta name="emotion-insertion-point" content="" />
        </Head>
        <body>
          <Main />
          <NextScript />
        </body>
      </Html>
    );
  }
}

フォントなどの外部リソースをプリロードすることで、スタイル適用に必要な要素を早期に読み込めます。

解決策 5:developmentMode での確認とデバッグ

Emotion は開発モードとプロダクションモードで動作が異なります。

開発中は人間が読みやすいクラス名が生成されますが、本番ではハッシュ化された短い名前が使われます。FOUC の調査時には、ブラウザの開発者ツールで <style> タグの挿入状態を確認し、スタイルが正しいタイミングで適用されているかをチェックしましょう。

以下は、Chrome DevTools でスタイル挿入を確認する手順です。

#手順確認内容
1ページを開き、DevTools の Elements タブを表示<head> 内の <style> タグの有無
2Network タブでスタイルシートの読み込みタイミングをチェックJavaScript ロード前にスタイルが存在するか
3Performance タブで初回レンダリングを記録スタイル適用前の描画(FOUC)が発生していないか

この確認により、スタイルが SSR で埋め込まれているか、クライアント側で遅延して挿入されているかを判別できます。

具体例

具体例 1:Next.js での完全な実装例

ここでは、Next.js プロジェクトで Emotion を使い、FOUC を完全に防ぐための実装例を段階的に紹介します。

まず、プロジェクトに必要なパッケージをインストールします。

bash# Emotion 本体と React 用パッケージ、SSR サーバーユーティリティをインストール
yarn add @emotion/react @emotion/cache @emotion/server

必要なパッケージは、@emotion​/​react(コンポーネントでの利用)、@emotion​/​cache(キャッシュ管理)、@emotion​/​server(SSR 対応)の 3 つです。

次に、キャッシュ作成関数を実装します。

typescript// lib/createEmotionCache.ts
import createCache from '@emotion/cache';

/**
 * Emotion のスタイルキャッシュを生成
 * - prepend: true でスタイルを <head> の先頭に挿入
 * - insertionPoint で挿入位置を制御(ブラウザ環境のみ)
 */
export function createEmotionCache() {
  const isBrowser = typeof document !== 'undefined';

  return createCache({
    key: 'css',
    prepend: true,
    insertionPoint: isBrowser
      ? document.querySelector<HTMLMetaElement>('#emotion-insertion-point')!
      : undefined,
  });
}

この関数により、サーバー・クライアント両方で一貫したキャッシュ設定を利用できます。

続いて、_app.tsx でキャッシュプロバイダーを設定します。

typescript// pages/_app.tsx
import type { AppProps } from 'next/app';
import { CacheProvider, EmotionCache } from '@emotion/react';
import { createEmotionCache } from '../lib/createEmotionCache';

// クライアント側で使うデフォルトキャッシュ
const clientSideEmotionCache = createEmotionCache();

interface MyAppProps extends AppProps {
  emotionCache?: EmotionCache;
}

/**
 * アプリ全体を CacheProvider でラップし、
 * すべてのコンポーネントで同じキャッシュを共有
 */
function MyApp({
  Component,
  pageProps,
  emotionCache = clientSideEmotionCache,
}: MyAppProps) {
  return (
    <CacheProvider value={emotionCache}>
      <Component {...pageProps} />
    </CacheProvider>
  );
}

export default MyApp;

emotionCache をプロパティで受け取ることで、SSR 時にサーバー側で生成したキャッシュをクライアント側でも再利用できます。

次に、_document.tsx で SSR 時のスタイル抽出を実装します。

typescript// pages/_document.tsx
import Document, {
  Html,
  Head,
  Main,
  NextScript,
  DocumentContext,
  DocumentInitialProps,
} from 'next/document';
import createEmotionServer from '@emotion/server/create-instance';
import { CacheProvider } from '@emotion/react';
import { createEmotionCache } from '../lib/createEmotionCache';

/**
 * サーバーサイドで Emotion スタイルを抽出し、
 * HTML に埋め込むカスタム Document
 */
class MyDocument extends Document {
  static async getInitialProps(
    ctx: DocumentContext
  ): Promise<DocumentInitialProps> {
    // サーバー専用キャッシュを生成
    const cache = createEmotionCache();
    const { extractCriticalToChunks } = createEmotionServer(cache);

    const originalRenderPage = ctx.renderPage;

    // レンダリング時に CacheProvider でラップ
    ctx.renderPage = () =>
      originalRenderPage({
        enhanceApp: (App) => (props) =>
          (
            <CacheProvider value={cache}>
              <App {...props} />
            </CacheProvider>
          ),
      });

    const initialProps = await Document.getInitialProps(ctx);

    // スタイルを抽出
    const emotionStyles = extractCriticalToChunks(initialProps.html);
    const emotionStyleTags = emotionStyles.styles.map((style) => (
      <style
        data-emotion={`${style.key} ${style.ids.join(' ')}`}
        key={style.key}
        dangerouslySetInnerHTML={{ __html: style.css }}
      />
    ));

    return {
      ...initialProps,
      styles: [...emotionStyleTags, ...initialProps.styles],
    };
  }

  render() {
    return (
      <Html lang="ja">
        <Head>
          {/* Emotion スタイル挿入の基準点 */}
          <meta name="emotion-insertion-point" content="" />
        </Head>
        <body>
          <Main />
          <NextScript />
        </body>
      </Html>
    );
  }
}

export default MyDocument;

この設定により、サーバーでレンダリングされた HTML に必要なスタイルがすべて埋め込まれ、ブラウザが受け取った時点でスタイルが適用されている状態になります。

最後に、実際のコンポーネントで Emotion を使う例です。

typescript// components/Button.tsx
import { css } from '@emotion/react';

/**
 * Emotion でスタイル定義した Button コンポーネント
 */
const buttonStyle = css`
  background-color: #0070f3;
  color: white;
  padding: 12px 24px;
  border: none;
  border-radius: 8px;
  font-size: 16px;
  cursor: pointer;
  transition: background-color 0.2s;

  &:hover {
    background-color: #005bb5;
  }
`;

css 関数を使ってスタイルを定義し、コンポーネント内で再利用可能にします。

typescriptinterface ButtonProps {
  children: React.ReactNode;
  onClick?: () => void;
}

export function Button({ children, onClick }: ButtonProps) {
  return (
    <button css={buttonStyle} onClick={onClick}>
      {children}
    </button>
  );
}

このコンポーネントは、SSR 時にスタイルが HTML に埋め込まれるため、FOUC が発生しません。

具体例 2:MUI との併用時の注意点

Material-UI(MUI)と Emotion を併用する場合、両者のスタイル挿入順序が競合することがあります。

MUI も内部で Emotion を使っているため、スタイルの詳細度が意図しない形で逆転する可能性があるのです。この問題を解決するには、StyledEngineProvider を使って挿入順序を制御します。

以下は、MUI と Emotion を併用する場合の _app.tsx の設定例です。

typescript// pages/_app.tsx(MUI 併用版)
import type { AppProps } from 'next/app';
import { CacheProvider, EmotionCache } from '@emotion/react';
import { StyledEngineProvider } from '@mui/material/styles';
import { createEmotionCache } from '../lib/createEmotionCache';

const clientSideEmotionCache = createEmotionCache();

interface MyAppProps extends AppProps {
  emotionCache?: EmotionCache;
}

/**
 * MUI のスタイルを Emotion より後に適用させるため、
 * StyledEngineProvider で injectFirst を有効化
 */
function MyApp({
  Component,
  pageProps,
  emotionCache = clientSideEmotionCache,
}: MyAppProps) {
  return (
    <CacheProvider value={emotionCache}>
      <StyledEngineProvider injectFirst>
        <Component {...pageProps} />
      </StyledEngineProvider>
    </CacheProvider>
  );
}

export default MyApp;

injectFirst により、MUI のスタイルが Emotion のスタイルより に挿入されるため、詳細度の競合を防げます。

以下の表は、併用時のスタイル優先順位を整理したものです。

#ライブラリ挿入順序優先度
1Emotion(独自スタイル)先頭(prepend: true)
2MUI(コンポーネントスタイル)後(injectFirst)
3カスタム CSS(外部ファイル)最後

この順序を守ることで、意図したスタイルが確実に適用されます。

具体例 3:TypeScript での型安全な実装

Emotion は TypeScript との相性が良く、型定義を活用することでスタイルのプロパティを型安全に扱えます。

@emotion​/​react のテーマ機能を使えば、デザインシステム全体で統一されたカラーやフォントサイズを型チェック付きで利用できるでしょう。

まず、テーマの型定義とテーマオブジェクトを作成します。

typescript// styles/theme.ts
import { Theme } from '@emotion/react';

/**
 * カスタムテーマの型定義
 * アプリ全体で使うカラーやフォントサイズを一元管理
 */
declare module '@emotion/react' {
  export interface Theme {
    colors: {
      primary: string;
      secondary: string;
      background: string;
      text: string;
    };
    fontSizes: {
      small: string;
      medium: string;
      large: string;
    };
  }
}

declare module を使って Emotion の Theme 型を拡張し、独自のプロパティを追加します。

typescript/**
 * 実際のテーマオブジェクト
 */
export const theme: Theme = {
  colors: {
    primary: '#0070f3',
    secondary: '#ff4081',
    background: '#ffffff',
    text: '#333333',
  },
  fontSizes: {
    small: '12px',
    medium: '16px',
    large: '24px',
  },
};

このテーマオブジェクトを ThemeProvider で提供することで、すべてのコンポーネントで利用できるようになります。

次に、_app.tsx でテーマプロバイダーを設定します。

typescript// pages/_app.tsx(テーマ適用版)
import type { AppProps } from 'next/app';
import { CacheProvider, EmotionCache, ThemeProvider } from '@emotion/react';
import { createEmotionCache } from '../lib/createEmotionCache';
import { theme } from '../styles/theme';

const clientSideEmotionCache = createEmotionCache();

interface MyAppProps extends AppProps {
  emotionCache?: EmotionCache;
}

/**
 * ThemeProvider でアプリ全体にテーマを提供
 */
function MyApp({
  Component,
  pageProps,
  emotionCache = clientSideEmotionCache,
}: MyAppProps) {
  return (
    <CacheProvider value={emotionCache}>
      <ThemeProvider theme={theme}>
        <Component {...pageProps} />
      </ThemeProvider>
    </CacheProvider>
  );
}

export default MyApp;

これにより、すべてのコンポーネントでテーマオブジェクトにアクセスできます。

最後に、コンポーネント内でテーマを活用する例です。

typescript// components/Card.tsx
import { css } from '@emotion/react';
import { useTheme } from '@emotion/react';

/**
 * テーマを活用した Card コンポーネント
 */
export function Card({ children }: { children: React.ReactNode }) {
  const theme = useTheme();

  // テーマの値を使ってスタイルを定義
  const cardStyle = css`
    background-color: ${theme.colors.background};
    color: ${theme.colors.text};
    padding: 24px;
    border-radius: 8px;
    box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
    font-size: ${theme.fontSizes.medium};
  `;

  return <div css={cardStyle}>{children}</div>;
}

useTheme フックでテーマオブジェクトを取得し、型安全にスタイルを定義できます。TypeScript のコード補完も効くため、タイポを防ぎながら開発できるでしょう。

具体例 4:エラーハンドリングとデバッグ

Emotion で FOUC が発生した場合、開発者ツールを使ってスタイルの挿入状態を確認することが重要です。

以下では、よくあるエラーとその解決方法を紹介します。

エラー 1:TypeError: Cannot read property 'prepend' of undefined

このエラーは、createEmotionCache がサーバー側で document にアクセスしようとした際に発生します。

typescript// エラーが発生するコード例
export function createEmotionCache() {
  return createCache({
    key: 'css',
    prepend: true,
    insertionPoint: document.querySelector('#emotion-insertion-point')!, // サーバーでは document が未定義
  });
}

エラーコード: TypeError: Cannot read property 'querySelector' of undefined

発生条件:

  • SSR 実行時に document が存在しない環境でアクセスした場合
  • insertionPoint にサーバー側で評価される DOM 操作が含まれている場合

解決方法:

  1. ブラウザ環境かどうかを判定する条件分岐を追加
  2. サーバー側では insertionPointundefined にする
  3. クライアント側のみで DOM 要素を参照する
typescript// 修正後のコード
export function createEmotionCache() {
  const isBrowser = typeof document !== 'undefined';

  return createCache({
    key: 'css',
    prepend: true,
    insertionPoint: isBrowser
      ? document.querySelector<HTMLMetaElement>('#emotion-insertion-point')!
      : undefined,
  });
}

エラー 2:Warning: Prop className did not match

このエラーは、サーバーで生成されたクラス名とクライアントで生成されたクラス名が一致しない場合に発生します。

エラーコード: Warning: Prop className did not match. Server: "css-abc123" Client: "css-def456"

発生条件:

  • サーバーとクライアントで異なるキャッシュ設定を使っている
  • _document.tsx でスタイル抽出が正しく行われていない
  • 動的なスタイル生成ロジックがサーバーとクライアントで異なる

解決方法:

  1. _app.tsx_document.tsx で同じ createEmotionCache 関数を使う
  2. getInitialProps でスタイルを確実に抽出する
  3. サーバー・クライアント両方で同じ key を使う
typescript// _document.tsx で確実にスタイルを抽出
static async getInitialProps(ctx: DocumentContext) {
  const cache = createEmotionCache();
  const { extractCriticalToChunks } = createEmotionServer(cache);

  // renderPage をオーバーライド
  const originalRenderPage = ctx.renderPage;
  ctx.renderPage = () =>
    originalRenderPage({
      enhanceApp: (App) => (props) => (
        <CacheProvider value={cache}>
          <App {...props} />
        </CacheProvider>
      ),
    });

  const initialProps = await Document.getInitialProps(ctx);
  const emotionStyles = extractCriticalToChunks(initialProps.html);
  const emotionStyleTags = emotionStyles.styles.map((style) => (
    <style
      data-emotion={`${style.key} ${style.ids.join(' ')}`}
      key={style.key}
      dangerouslySetInnerHTML={{ __html: style.css }}
    />
  ));

  return {
    ...initialProps,
    styles: [...emotionStyleTags, ...initialProps.styles],
  };
}

参考リンク:

以下の表は、デバッグ時に確認すべきポイントをまとめたものです。

#確認項目確認方法期待される状態
1サーバー HTML にスタイルが含まれているかページソースを表示し、<style data-emotion> タグを探すサーバー HTML に <style> タグが存在する
2クライアント側でスタイルが再挿入されていないかDevTools の Elements タブで <head> 内の変化を監視追加の <style> タグが挿入されない
3キャッシュキーが一致しているかdata-emotion 属性の値を確認サーバー・クライアント両方で同じキー(例:css
4ハイドレーションエラーが出ていないかConsole タブでエラーメッセージを確認Warning や Error が表示されない

これらの確認により、FOUC の原因を特定し、適切な対処ができるでしょう。

まとめ

Emotion で発生する FOUC は、スタイルの挿入タイミングや順序、SSR 時の抽出処理など、複数の要因が絡み合って起こる問題です。

しかし、createEmotionCache によるキャッシュ制御SSR でのスタイル抽出と HTML への埋め込み挿入順序の明示的な管理プリロード設定の最適化 という 4 つのアプローチを組み合わせることで、FOUC を完全に防ぐことができます。

本記事で紹介した実装例は、Next.js をはじめとする SSR フレームワークで Emotion を使う際のベストプラクティスとなるでしょう。特に、_document.tsx での extractCriticalToChunks によるスタイル抽出は、サーバーで生成された HTML に必要なスタイルをすべて含めることができるため、初回表示時の FOUC を確実に防げます。

また、MUI などの他の CSS-in-JS ライブラリと併用する場合は、insertionPointStyledEngineProvider を使って挿入順序を制御することが重要です。TypeScript の型定義を活用すれば、テーマの型安全性も確保でき、大規模なプロジェクトでも安心して開発を進められますね。

FOUC の問題に悩んでいる方は、ぜひ本記事の手順を参考に、段階的に実装を見直してみてください。適切な設定により、ユーザーに快適な表示体験を提供できるはずです。

関連リンク