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 | スタイル生成のオーバーヘッド | 初回レンダリングの遅延 |
| 2 | DOM への挿入コスト | レイアウトの再計算 |
| 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 は以下の処理を実行します。
buttonStyleのハッシュ値を計算(例:1j8o68f)- クラス名を生成(例:
css-1j8o68f) - キャッシュをチェック
- 必要に応じて
<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 キーまたは右クリック → 検証 |
| 2 | Elements タブを選択 | DOM ツリーを表示 |
| 3 | <head> 要素を展開 | ヘッダー内の要素を確認 |
| 4 | data-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 の裏側で高度な最適化を行っており、開発者体験とパフォーマンスの両立を実現しています。
関連リンク
articleEmotion の仕組みを図解で解説:ランタイム生成・ハッシュ化・挿入順序の全貌
articleEmotion のパフォーマンス監視運用:Web Vitals× トレースでボトルネック特定
articleEmotion で FOUC が出る原因と解決策:挿入順序/SSR 抽出/プリロードの総点検
articleEmotion で B2B 管理画面を高速構築:テーブル/フィルタ/フォームの型安全化
articleEmotion で「ボタンの設計システム」を構築:サイズ/色/状態/アイコンを型安全に
articleEmotion の「変種(variants)」設計パターン:props→ スタイルの型安全マッピング
articleGPT-5 構造化出力チートシート:JSON/表/YAML/コードブロックの安定生成パターン
articleESLint 変更管理と段階リリース:CI のフェイルセーフ&ロールバック手順
articleDify フィードバック学習運用:人手評価・プロンプト AB テスト・継続改善
articleFlutter で業務用管理画面:テーブル・フィルタ・エクスポート機能の実装指針
articleDeno で Permission Denied が出る理由と解決手順:--allow-\* フラグ総点検
articleEmotion の仕組みを図解で解説:ランタイム生成・ハッシュ化・挿入順序の全貌
blogiPhone 17シリーズの発表!全モデルiPhone 16から進化したポイントを見やすく整理
blogGoogleストアから訂正案内!Pixel 10ポイント有効期限「1年」表示は誤りだった
blog【2025年8月】Googleストア「ストアポイント」は1年表記はミス?2年ルールとの整合性を検証
blogGoogleストアの注文キャンセルはなぜ起きる?Pixel 10購入前に知るべき注意点
blogPixcel 10シリーズの発表!全モデル Pixcel 9 から進化したポイントを見やすく整理
blogフロントエンドエンジニアの成長戦略:コーチングで最速スキルアップする方法
review今の自分に満足していますか?『持たざる者の逆襲 まだ何者でもない君へ』溝口勇児
reviewついに語られた業界の裏側!『フジテレビの正体』堀江貴文が描くテレビ局の本当の姿
review愛する勇気を持てば人生が変わる!『幸せになる勇気』岸見一郎・古賀史健のアドラー実践編で真の幸福を手に入れる
review週末を変えれば年収も変わる!『世界の一流は「休日」に何をしているのか』越川慎司の一流週末メソッド
review新しい自分に会いに行こう!『自分の変え方』村岡大樹の認知科学コーチングで人生リセット
review科学革命から AI 時代へ!『サピエンス全史 下巻』ユヴァル・ノア・ハラリが予見する人類の未来