T-CREATOR

Emotion の仕組みを図解で解説:ランタイム生成・ハッシュ化・挿入順序の全貌

Emotion の仕組みを図解で解説:ランタイム生成・ハッシュ化・挿入順序の全貌

React でスタイリングを行う際、CSS-in-JS ライブラリの選択肢は数多くありますが、その中でも Emotion は高いパフォーマンスと優れた開発者体験を両立させています。

本記事では、Emotion がどのようにしてスタイルを生成し、管理しているのか、その内部メカニズムに焦点を当てて解説いたします。特に「ランタイム生成」「ハッシュ化」「挿入順序」という 3 つの核心的な仕組みを、図解を交えながら詳しく見ていきましょう。

これらの仕組みを理解することで、Emotion をより効果的に活用でき、パフォーマンスチューニングやデバッグの際にも役立つはずです。

背景

CSS-in-JS が解決した課題

従来の CSS では、グローバルスコープによる名前の衝突や、JavaScript との連携の難しさが課題となっていました。

CSS-in-JS はこれらの問題を解決するために登場し、スタイルをコンポーネントと同じ場所で管理できるようになりました。特に React エコシステムでは、コンポーネント指向の開発スタイルと相性が良く、急速に普及していきました。

Emotion が選ばれる理由

Emotion は CSS-in-JS ライブラリの中でも、以下の特徴により多くの開発者から支持されています。

第一に、軽量で高速な実行速度です。バンドルサイズが小さく、ランタイムのオーバーヘッドも最小限に抑えられています。

第二に、柔軟な API 設計です。css プロパティを使った直感的な記法から、styled コンポーネントまで、開発者の好みに合わせた記述が可能です。

下図は、CSS-in-JS の進化と Emotion の位置づけを示しています。

mermaidflowchart TB
    traditional["従来の CSS"]
    cssinjs["CSS-in-JS の登場"]
    emotion["Emotion"]

    traditional -->|課題| prob1["グローバルスコープ"]
    traditional -->|課題| prob2["JS との連携困難"]

    prob1 --> cssinjs
    prob2 --> cssinjs

    cssinjs -->|進化| emotion

    emotion -->|特徴1| feature1["軽量・高速"]
    emotion -->|特徴2| feature2["柔軟な API"]
    emotion -->|特徴3| feature3["優れた DX"]

この図から、Emotion が従来の課題を解決しつつ、さらなる進化を遂げていることがわかります。

パフォーマンスと開発者体験の両立

CSS-in-JS ライブラリは、実行時にスタイルを生成するため、パフォーマンスへの影響が懸念されることがあります。

Emotion はこの課題に対して、効率的なハッシュ化とキャッシング機構を導入することで対応しました。同じスタイルは一度しか生成されず、再利用される仕組みになっています。

また、開発時のホットリロードやソースマップのサポートなど、開発者体験も重視されています。

課題

スタイル管理の複雑さ

動的にスタイルを生成する場合、以下のような課題が発生します。

まず、同じスタイルが重複して生成されるとパフォーマンスが低下します。メモリ使用量も増加し、DOM 操作のコストも高くなってしまいます。

次に、スタイルの適用順序が不確定だと、意図しない上書きが発生する可能性があります。CSS の詳細度だけでなく、記述順序も重要な要素です。

パフォーマンスの懸念

ランタイムでスタイルを生成する場合、以下のパフォーマンス上の課題があります。

#課題影響
1スタイル生成のオーバーヘッド初回レンダリングの遅延
2DOM への挿入コストレイアウトの再計算
3メモリ使用量の増加ブラウザのリソース圧迫
4重複したスタイルの生成無駄な処理の発生

これらの課題を解決するには、効率的なキャッシング機構と最適化されたスタイル生成アルゴリズムが必要です。

スタイルの衝突問題

複数のコンポーネントで似たようなスタイルを定義すると、予期しない衝突が発生することがあります。

下図は、従来の方法で発生しうる課題を示しています。

mermaidflowchart TD
    comp1["コンポーネント A"]
    comp2["コンポーネント B"]

    comp1 -->|生成| style1[".button { color: red }"]
    comp2 -->|生成| style2[".button { color: blue }"]

    style1 --> dom["DOM"]
    style2 --> dom

    dom -->|結果| conflict["衝突:どちらが適用される?"]

    conflict -->|問題1| issue1["予測困難"]
    conflict -->|問題2| issue2["デバッグ困難"]
    conflict -->|問題3| issue3["保守性の低下"]

このような衝突を防ぐためには、各スタイルに一意な識別子を付与する必要があります。

解決策

ランタイム生成の仕組み

Emotion は、スタイルをランタイムで動的に生成します。この仕組みについて、段階的に見ていきましょう。

スタイルオブジェクトの作成

まず、開発者が記述したスタイル定義から、内部的なスタイルオブジェクトが作成されます。

typescriptimport { css } from '@emotion/react';

// スタイル定義
const buttonStyle = css({
  backgroundColor: 'blue',
  color: 'white',
  padding: '10px 20px',
  borderRadius: '4px',
});

この css 関数が呼ばれた時点で、Emotion の内部処理が開始されます。

CSS 文字列への変換

次に、スタイルオブジェクトが CSS 文字列に変換されます。この処理では、JavaScript のオブジェクト記法が標準的な CSS 記法に変換されます。

typescript// 内部的に以下のような変換が行われる
const cssString = `
  background-color: blue;
  color: white;
  padding: 10px 20px;
  border-radius: 4px;
`;

プロパティ名のキャメルケースからケバブケースへの変換や、ベンダープレフィックスの自動付与なども、この段階で行われます。

キャッシュの確認

生成された CSS 文字列は、すぐには DOM に挿入されません。まず、キャッシュに同じスタイルが存在するかチェックされます。

typescript// 疑似コード:Emotion の内部処理イメージ
function insertStyles(cssString: string) {
  // キャッシュキーの生成
  const cacheKey = generateHash(cssString);

  // キャッシュに存在するかチェック
  if (cache.has(cacheKey)) {
    // 既存のクラス名を返す
    return cache.get(cacheKey);
  }

  // 新規スタイルの場合は DOM に挿入
  return insertNewStyle(cssString, cacheKey);
}

この仕組みにより、同じスタイルが複数回生成されることを防いでいます。

ハッシュ化によるユニーク性

Emotion の最も重要な仕組みの一つが、スタイルのハッシュ化です。

ハッシュ生成のアルゴリズム

Emotion は、スタイルの内容から一意なハッシュ値を生成します。このハッシュ値は、クラス名の一部として使用されます。

typescript// ハッシュ生成の例
function hashString(str: string): string {
  let hash = 0;

  // 文字列の各文字からハッシュ値を計算
  for (let i = 0; i < str.length; i++) {
    const char = str.charCodeAt(i);
    hash = (hash << 5) - hash + char;
    hash = hash & hash; // 32bit整数に変換
  }

  // Base36形式で文字列化
  return Math.abs(hash).toString(36);
}

この関数により、同じスタイルからは常に同じハッシュが生成され、異なるスタイルからは異なるハッシュが生成されます。

クラス名の生成

生成されたハッシュ値は、プレフィックスと組み合わせてクラス名になります。

typescript// クラス名生成の例
function generateClassName(hash: string): string {
  // "css-" というプレフィックスとハッシュを結合
  return `css-${hash}`;
}

// 例:css-1j8o68f

このクラス名は、開発者ツールで確認する際にも表示されるため、デバッグ時の手がかりになります。

ハッシュの衝突対策

理論上、異なるスタイルから同じハッシュが生成される可能性はゼロではありません。

Emotion は、この衝突リスクを最小化するために、以下の対策を実装しています。

typescript// 衝突検出と対処の疑似コード
function safeInsertStyle(cssString: string, hash: string) {
  const className = `css-${hash}`;

  // 既存のスタイルを取得
  const existingStyle = cache.get(hash);

  if (existingStyle && existingStyle !== cssString) {
    // 衝突が検出された場合
    console.warn('Hash collision detected');
    // 追加のサフィックスを付与
    return generateClassName(hash + '_alt');
  }

  // 衝突がない場合は通常通り挿入
  cache.set(hash, cssString);
  return className;
}

実際には、ハッシュ関数の品質が高いため、衝突はほとんど発生しません。

挿入順序の制御

CSS では、同じ詳細度を持つルールが複数ある場合、後から定義されたものが優先されます。

Emotion は、この挿入順序を適切に制御することで、スタイルの上書きを予測可能にしています。

スタイルシートの管理

Emotion は、<style> タグを動的に作成し、<head> 内に挿入します。

typescript// スタイルシート作成の例
function createStyleSheet(): HTMLStyleElement {
  // style要素を作成
  const style = document.createElement('style');

  // data属性でEmotionのスタイルであることを示す
  style.setAttribute('data-emotion', 'css');

  // headの最後に追加
  document.head.appendChild(style);

  return style;
}

この data-emotion 属性により、開発者ツールでも Emotion によるスタイルであることが識別できます。

挿入順序の保証

スタイルは、コンポーネントのレンダリング順序に従って挿入されます。

typescript// 挿入順序管理の疑似コード
class StyleSheet {
  private insertionPoint: number = 0;

  insert(cssRule: string) {
    // 現在の挿入位置にルールを追加
    this.sheet.insertRule(cssRule, this.insertionPoint);

    // 次の挿入位置を更新
    this.insertionPoint++;
  }
}

この仕組みにより、後から定義されたスタイルが確実に優先されます。

SSR との連携

サーバーサイドレンダリング(SSR)時にも、同じ挿入順序が保証される必要があります。

typescript// SSR時のスタイル抽出例
import { renderToString } from 'react-dom/server';
import { CacheProvider } from '@emotion/react';
import createEmotionServer from '@emotion/server/create-instance';
import createCache from '@emotion/cache';

// キャッシュを作成
const cache = createCache({ key: 'css' });
const {
  extractCriticalToChunks,
  constructStyleTagsFromChunks,
} = createEmotionServer(cache);

// HTMLを生成
const html = renderToString(
  <CacheProvider value={cache}>
    <App />
  </CacheProvider>
);

この実装により、サーバーとクライアントで同じスタイルが同じ順序で適用されます。

下図は、Emotion の内部処理フローの全体像を示しています。

mermaidsequenceDiagram
    participant Dev as 開発者
    participant Emotion as Emotion
    participant Cache as キャッシュ
    participant Hash as ハッシュ生成
    participant DOM as DOM

    Dev->>Emotion: css関数を呼び出し
    Emotion->>Emotion: スタイルオブジェクト作成
    Emotion->>Hash: CSS文字列を渡す
    Hash->>Hash: ハッシュ値を計算
    Hash->>Cache: ハッシュをキーに検索

    alt キャッシュに存在
        Cache-->>Emotion: 既存のクラス名を返す
    else キャッシュに存在しない
        Cache->>DOM: 新規スタイルを挿入
        DOM-->>Cache: 挿入完了
        Cache-->>Emotion: 新しいクラス名を返す
    end

    Emotion-->>Dev: クラス名を返す

この図から、Emotion がスタイルを効率的に管理していることが理解できます。

図で理解できる要点:

  • スタイルは必ずキャッシュチェックを経由する
  • 同じスタイルは一度しか DOM に挿入されない
  • ハッシュ化により一意性が保証される

具体例

基本的な使用例

実際のコンポーネントで、Emotion がどのように動作するか見ていきましょう。

シンプルなボタンコンポーネント

まず、最もシンプルな例から始めます。

typescriptimport { css } from '@emotion/react';

// スタイル定義
const buttonStyle = css({
  backgroundColor: '#007bff',
  color: 'white',
  padding: '10px 20px',
  border: 'none',
  borderRadius: '4px',
  cursor: 'pointer',
  fontSize: '16px',
});

この定義により、Emotion は内部的にハッシュ値を生成します。

コンポーネントでの使用

生成されたスタイルをコンポーネントに適用します。

typescriptfunction Button({
  children,
}: {
  children: React.ReactNode;
}) {
  return <button css={buttonStyle}>{children}</button>;
}

レンダリング時、Emotion は以下の処理を実行します。

  1. buttonStyle のハッシュ値を計算(例:1j8o68f
  2. クラス名を生成(例:css-1j8o68f
  3. キャッシュをチェック
  4. 必要に応じて <style> タグに CSS を挿入

ハッシュ生成の実例

実際にどのようなハッシュが生成されるか、複数のパターンで確認してみましょう。

異なるスタイルの場合

typescript// スタイル1
const style1 = css({
  color: 'red',
  fontSize: '14px',
});
// 生成されるクラス名: css-1kw4g2h

// スタイル2(異なる内容)
const style2 = css({
  color: 'blue',
  fontSize: '14px',
});
// 生成されるクラス名: css-1xj92kf

内容が異なるため、異なるハッシュが生成されます。

同じスタイルの場合

typescript// コンポーネントA
const styleA = css({
  padding: '10px',
  margin: '5px',
});

// コンポーネントB(同じ内容)
const styleB = css({
  padding: '10px',
  margin: '5px',
});

// styleA と styleB は同じハッシュ値を持つ
// 生成されるクラス名: css-1hb7qs8(両方とも)

同じ内容のため、同じハッシュが生成され、スタイルは一度だけ挿入されます。

プロパティの順序が異なる場合

typescript// スタイルC
const styleC = css({
  color: 'green',
  fontSize: '16px',
});

// スタイルD(順序が異なる)
const styleD = css({
  fontSize: '16px',
  color: 'green',
});

Emotion は、オブジェクトのプロパティを正規化してからハッシュ化するため、順序が異なっても同じハッシュが生成されます。

挿入順序の確認方法

ブラウザの開発者ツールで、実際の挿入順序を確認できます。

開発者ツールでの確認手順

以下の手順で、Emotion が生成したスタイルを確認できます。

#手順説明
1開発者ツールを開くF12 キーまたは右クリック → 検証
2Elements タブを選択DOM ツリーを表示
3<head> 要素を展開ヘッダー内の要素を確認
4data-emotion 属性を探すEmotion のスタイルタグを特定
5スタイルの内容を確認各クラスと CSS ルールをチェック

実際の DOM 構造

Emotion が生成する DOM 構造は以下のようになります。

html<head>
  <!-- その他のメタタグなど -->

  <!-- Emotionによって生成されたスタイルタグ -->
  <style data-emotion="css">
    .css-1j8o68f {
      background-color: #007bff;
      color: white;
      padding: 10px 20px;
      border: none;
      border-radius: 4px;
      cursor: pointer;
      font-size: 16px;
    }
  </style>

  <style data-emotion="css">
    .css-1kw4g2h {
      color: red;
      font-size: 14px;
    }
  </style>

  <!-- 後から追加されたスタイルは下に配置される -->
</head>

この構造から、スタイルが追加された順序を確認できます。

動的なスタイル生成

Props に応じてスタイルを動的に変更する例を見てみましょう。

Props を受け取るボタン

typescriptinterface ButtonProps {
  variant: 'primary' | 'secondary' | 'danger';
  children: React.ReactNode;
}

function DynamicButton({ variant, children }: ButtonProps) {
  // variantに応じてスタイルを生成
  const buttonStyle = css({
    padding: '10px 20px',
    border: 'none',
    borderRadius: '4px',
    cursor: 'pointer',
    fontSize: '16px',
    // variant によって色を変更
    backgroundColor:
      variant === 'primary' ? '#007bff' :
      variant === 'secondary' ? '#6c757d' :
      '#dc3545',
    color: 'white',
  });

このコンポーネントは、variant の値ごとに異なるハッシュを生成します。

レンダリング結果

typescript  return <button css={buttonStyle}>{children}</button>;
}

// 使用例
function App() {
  return (
    <div>
      <DynamicButton variant="primary">Primary</DynamicButton>
      <DynamicButton variant="secondary">Secondary</DynamicButton>
      <DynamicButton variant="danger">Danger</DynamicButton>
    </div>
  );
}

この例では、3 つの異なるスタイルが生成され、それぞれ異なるクラス名が付与されます。

下図は、動的スタイル生成の処理フローを示しています。

mermaidflowchart TD
    start["コンポーネント<br/>レンダリング"]
    props["Props を取得<br/>(variant)"]
    generate["スタイルオブジェクト<br/>を生成"]
    hash["ハッシュ値<br/>を計算"]

    start --> props
    props --> generate
    generate --> hash

    hash --> check{"キャッシュに<br/>存在?"}

    check -->|Yes| reuse["既存のクラス名<br/>を再利用"]
    check -->|No| insert["新規スタイルを<br/>DOM に挿入"]

    reuse --> apply["クラス名を<br/>要素に適用"]
    insert --> apply

    apply --> complete["レンダリング<br/>完了"]

この図から、Props の値によってスタイルが動的に変わる様子が理解できます。

パフォーマンスの測定

実際にどれくらいのパフォーマンスが出るか、簡単な測定を行ってみましょう。

測定コード

typescriptimport { css } from '@emotion/react';

// 同じスタイルを1000回生成
console.time('同じスタイルを1000回生成');
for (let i = 0; i < 1000; i++) {
  const style = css({
    color: 'red',
    fontSize: '14px',
  });
}
console.timeEnd('同じスタイルを1000回生成');

この測定により、キャッシング機構の効果を確認できます。

測定結果の考察

同じスタイルを何度生成しても、実際に DOM に挿入されるのは 1 回だけです。

2 回目以降は、キャッシュから既存のクラス名が返されるため、ほぼオーバーヘッドがありません。一般的に、上記のコードは 5ms 以下で完了します。

これは、Emotion のハッシュベースのキャッシング機構が非常に効率的であることを示しています。

まとめ

本記事では、Emotion の内部メカニズムについて、ランタイム生成、ハッシュ化、挿入順序という 3 つの観点から詳しく解説しました。

Emotion のランタイム生成は、開発者が記述したスタイル定義を動的に CSS に変換し、効率的に DOM へ挿入する仕組みです。この処理により、JavaScript の柔軟性を活かしたスタイリングが可能になります。

ハッシュ化の仕組みは、各スタイルに一意な識別子を付与し、スタイルの衝突を防ぎます。同じスタイルは同じハッシュを生成するため、効率的なキャッシングが実現されています。

挿入順序の制御により、CSS の詳細度や記述順序に関する予測可能性が保たれます。SSR との連携も含め、あらゆる状況で一貫したスタイル適用が保証されます。

これらの仕組みを理解することで、Emotion をより深く理解し、適切に活用できるようになるでしょう。パフォーマンスチューニングやデバッグの際にも、この知識が役立つはずです。

Emotion は、シンプルな API の裏側で高度な最適化を行っており、開発者体験とパフォーマンスの両立を実現しています。

関連リンク