T-CREATOR

Tailwind CSS のクラスが消える/縮む原因を特定:ツリーシェイクと safelist 完全対策

Tailwind CSS のクラスが消える/縮む原因を特定:ツリーシェイクと safelist 完全対策

Tailwind CSS で開発していると、開発環境では正常に表示されていたスタイルが、本番ビルド後に突然消えてしまう経験はありませんか。

この問題は多くの開発者を悩ませていますが、実は Tailwind CSS の「ツリーシェイク」という仕組みが原因です。本記事では、なぜクラスが消えるのか、その原因を徹底的に解明し、safelist を使った確実な対策方法をご紹介します。

背景

Tailwind CSS のビルド最適化の仕組み

Tailwind CSS は、デフォルトで数千以上のユーティリティクラスを提供しています。しかし、実際のプロジェクトで使用するクラスはその一部に過ぎません。

そこで Tailwind CSS は、本番ビルド時に「使用されていないクラス」を自動的に削除する最適化を行います。この処理を ツリーシェイク(Tree Shaking) と呼びます。

以下の図は、Tailwind CSS のビルドプロセスを示したものです。

mermaidflowchart TB
  source["ソースコード<br/>(HTML/JSX/Vue)"] -->|スキャン| scanner["Content Scanner"]
  scanner -->|クラス名抽出| classes["使用クラス一覧"]
  classes -->|フィルタリング| purge["PurgeCSS/JIT"]
  purge -->|最適化| output["最終 CSS<br/>(数十 KB)"]

  allClasses["全 Tailwind クラス<br/>(数 MB)"] -.->|使用分のみ| purge

  style source fill:#e3f2fd
  style output fill:#c8e6c9
  style purge fill:#fff9c4

図の要点:

  • ソースコード内のクラス名を静的にスキャン
  • 検出されたクラスのみを CSS に含める
  • 未使用クラスは自動的に削除される

JIT モードとスキャンの仕組み

Tailwind CSS v3 以降では、JIT(Just-In-Time)モードがデフォルトになりました。JIT モードは、ファイルを監視してクラス名を検出し、必要な CSS のみをリアルタイムで生成します。

javascript// tailwind.config.js の content 設定
module.exports = {
  content: [
    './pages/**/*.{js,ts,jsx,tsx}',
    './components/**/*.{js,ts,jsx,tsx}',
  ],
};

上記の設定により、Tailwind は指定されたパターンに一致するファイルをスキャンし、そこに含まれるクラス名を検出するのです。

スキャン対象となるクラス名の条件

Tailwind のスキャナーは、以下のようなクラス名を検出できます。

javascript// ✓ 検出される例
<div className='bg-blue-500 text-white'>静的なクラス</div>;

const Card = () => (
  <div className='p-4 rounded-lg shadow-md'>
    完全な文字列として記述
  </div>
);

しかし、次のような動的な記述は検出されません。

javascript// ✗ 検出されない例
const color = 'blue'
<div className={`bg-${color}-500`}>動的生成</div>

// ✗ 検出されない例
const buttonClass = isActive ? 'bg-green-500' : 'bg-gray-500'
<button className={buttonClass.split(' ')[0]}>ボタン</button>

このように、Tailwind のスキャナーは 静的な文字列としてのクラス名 のみを認識します。

課題

クラスが消える具体的なケース

実際の開発では、以下のようなケースでクラスが消えてしまいます。

ケース 1:テンプレートリテラルでの動的生成

typescript// components/Badge.tsx
type BadgeProps = {
  color: 'red' | 'blue' | 'green';
  children: React.ReactNode;
};

const Badge: React.FC<BadgeProps> = ({
  color,
  children,
}) => {
  // ❌ 本番ビルドでクラスが消える
  return (
    <span
      className={`bg-${color}-500 text-white px-2 py-1`}
    >
      {children}
    </span>
  );
};

上記のコードでは、bg-red-500bg-blue-500bg-green-500 が動的に生成されるため、Tailwind のスキャナーが検出できません。

ケース 2:外部ライブラリや CMS からの動的クラス

typescript// pages/article/[id].tsx
import { getArticle } from '@/lib/cms';

export default function ArticlePage({ article }) {
  // ❌ CMS から取得したクラス名は検出されない
  return (
    <div className={article.customClass}>
      {/* article.customClass = "bg-purple-500 text-lg" など */}
      <h1>{article.title}</h1>
    </div>
  );
}

CMS(Contentful、microCMS など)から取得したクラス名は、ビルド時には存在しないため検出されません。

ケース 3:条件分岐での複雑な組み合わせ

typescript// components/Alert.tsx
type AlertType = 'success' | 'warning' | 'error' | 'info';

const Alert = ({
  type,
  message,
}: {
  type: AlertType;
  message: string;
}) => {
  const colorMap = {
    success: 'green',
    warning: 'yellow',
    error: 'red',
    info: 'blue',
  };

  // ❌ オブジェクトから取得したクラス名は検出されない
  const bgColor = `bg-${colorMap[type]}-100`;
  const textColor = `text-${colorMap[type]}-800`;

  return (
    <div className={`${bgColor} ${textColor} p-4 rounded`}>
      {message}
    </div>
  );
};

上記のように、オブジェクトを介してクラス名を生成する場合も検出されません。

なぜ検出されないのか

以下の図は、Tailwind のスキャナーがクラス名を検出できない仕組みを示しています。

mermaidflowchart LR
  code["コード"] -->|スキャン| scanner["正規表現マッチング"]
  scanner -->|完全一致| detected["検出成功"]
  scanner -->|部分文字列| notDetected["検出失敗"]

  detected --> build["CSS に含まれる"]
  notDetected --> purged["CSS から削除"]

  style detected fill:#c8e6c9
  style notDetected fill:#ffcdd2
  style purged fill:#ffcdd2

図の要点:

  • スキャナーは正規表現で完全なクラス名を探す
  • テンプレートリテラルは文字列として認識されない
  • 検出されないクラスは本番ビルドで削除される

Tailwind のスキャナーは、以下のような正規表現パターンでクラス名を検出します。

javascript// Tailwind の内部的なスキャンパターン(簡略版)
const classPattern = /[^<>"'`\s]*[^<>"'`\s:]/g;

このパターンは、文字列リテラルとして完全に記述されたクラス名のみをマッチさせます。したがって、`bg-${color}-500` のような部分的な文字列は検出できないのです。

開発環境では動作する理由

開発環境では、すべてのクラスが含まれたフル版の CSS が読み込まれます。一方、本番ビルドでは最適化されたミニマム版の CSS のみが含まれます。

#環境CSS サイズ含まれるクラス
1開発数 MB全クラス
2本番数十 KB使用クラスのみ

これが、開発環境では正常に表示されるのに、本番環境でスタイルが消える理由です。

解決策

safelist を使った確実な対策

safelist は、スキャンで検出されなくても強制的に CSS に含めるクラス名を指定できる機能です。これにより、動的に生成されるクラスも確実に本番ビルドに含められます。

基本的な safelist の設定

javascript// tailwind.config.js
module.exports = {
  content: [
    './pages/**/*.{js,ts,jsx,tsx}',
    './components/**/*.{js,ts,jsx,tsx}',
  ],
  // safelist 設定を追加
  safelist: ['bg-red-500', 'bg-blue-500', 'bg-green-500'],
};

上記の設定により、指定した 3 つのクラスは必ず CSS に含まれます。

正規表現パターンでの一括指定

個別にクラスを列挙するのは大変です。正規表現パターンを使えば、関連するクラスをまとめて指定できます。

javascript// tailwind.config.js
module.exports = {
  content: [
    './pages/**/*.{js,ts,jsx,tsx}',
    './components/**/*.{js,ts,jsx,tsx}',
  ],
  safelist: [
    // 背景色:red, blue, green の 100〜900 すべて
    {
      pattern:
        /bg-(red|blue|green)-(100|200|300|400|500|600|700|800|900)/,
    },
    // テキスト色も同様に
    {
      pattern:
        /text-(red|blue|green)-(100|200|300|400|500|600|700|800|900)/,
    },
  ],
};

この設定により、bg-red-100 から bg-green-900 まで、すべての組み合わせが CSS に含まれます。

バリアント(hover、focus など)も含める

Tailwind CSS では、hover:focus:dark: などのバリアントも使用できます。safelist でこれらも指定しましょう。

javascript// tailwind.config.js
module.exports = {
  content: [
    './pages/**/*.{js,ts,jsx,tsx}',
    './components/**/*.{js,ts,jsx,tsx}',
  ],
  safelist: [
    {
      pattern: /bg-(red|blue|green)-(100|500|900)/,
      // hover と focus バリアントも含める
      variants: ['hover', 'focus'],
    },
  ],
};

これにより、hover:bg-red-500focus:bg-blue-500 なども CSS に含まれます。

コンポーネント単位での safelist 管理

プロジェクトが大きくなると、safelist も肥大化します。コンポーネントごとに必要なクラスを明確にしましょう。

javascript// tailwind.config.js

// Badge コンポーネント用のクラス
const badgeSafelist = [
  {
    pattern: /bg-(red|blue|green|yellow|purple)-(500)/,
  },
  {
    pattern: /text-white/,
  },
];

// Alert コンポーネント用のクラス
const alertSafelist = [
  {
    pattern: /bg-(green|yellow|red|blue)-(100)/,
  },
  {
    pattern: /text-(green|yellow|red|blue)-(800)/,
  },
];

module.exports = {
  content: [
    './pages/**/*.{js,ts,jsx,tsx}',
    './components/**/*.{js,ts,jsx,tsx}',
  ],
  safelist: [...badgeSafelist, ...alertSafelist],
};

このように分類することで、どのコンポーネントがどのクラスを使用しているか管理しやすくなります。

CMS や外部データ用の safelist

CMS から取得するクラス名は予測できないため、幅広く safelist に含める必要があります。

javascript// tailwind.config.js
module.exports = {
  content: [
    './pages/**/*.{js,ts,jsx,tsx}',
    './components/**/*.{js,ts,jsx,tsx}',
  ],
  safelist: [
    // CMS で使用する可能性のあるすべての背景色
    {
      pattern:
        /bg-(slate|gray|zinc|neutral|stone|red|orange|amber|yellow|lime|green|emerald|teal|cyan|sky|blue|indigo|violet|purple|fuchsia|pink|rose)-(50|100|200|300|400|500|600|700|800|900|950)/,
    },
    // テキストサイズ
    {
      pattern:
        /text-(xs|sm|base|lg|xl|2xl|3xl|4xl|5xl|6xl|7xl|8xl|9xl)/,
    },
    // パディング・マージン
    {
      pattern:
        /(p|m|px|py|pt|pb|pl|pr|mx|my|mt|mb|ml|mr)-(0|1|2|3|4|5|6|8|10|12|16|20|24)/,
    },
  ],
};

上記の設定により、CMS から動的に挿入されるクラスも本番ビルドに含まれます。

具体例

実装例 1:Badge コンポーネントの修正

問題のあったコードを safelist で解決します。

Before(クラスが消える)

typescript// components/Badge.tsx
type BadgeProps = {
  color: 'red' | 'blue' | 'green';
  children: React.ReactNode;
};

const Badge: React.FC<BadgeProps> = ({
  color,
  children,
}) => {
  // ❌ 動的生成のため検出されない
  return (
    <span
      className={`bg-${color}-500 text-white px-2 py-1 rounded`}
    >
      {children}
    </span>
  );
};

export default Badge;

After(safelist で対応)

まず、tailwind.config.js に safelist を追加します。

javascript// tailwind.config.js
module.exports = {
  content: [
    './pages/**/*.{js,ts,jsx,tsx}',
    './components/**/*.{js,ts,jsx,tsx}',
  ],
  safelist: [
    // Badge コンポーネント用のクラス
    'bg-red-500',
    'bg-blue-500',
    'bg-green-500',
    'text-white',
    'px-2',
    'py-1',
    'rounded',
  ],
};

これで Badge コンポーネントは本番ビルドでも正常に動作します。

さらに良いアプローチ:静的クラスマップを使用

safelist に頼るだけでなく、コード側でも静的にクラスを定義することで、Tailwind のスキャナーが検出できるようにします。

typescript// components/Badge.tsx
type BadgeColor = 'red' | 'blue' | 'green';

type BadgeProps = {
  color: BadgeColor;
  children: React.ReactNode;
};

// ✓ 静的なクラスマップ(スキャナーが検出可能)
const colorClasses: Record<BadgeColor, string> = {
  red: 'bg-red-500',
  blue: 'bg-blue-500',
  green: 'bg-green-500',
};

const Badge: React.FC<BadgeProps> = ({
  color,
  children,
}) => {
  // ✓ オブジェクトから完全なクラス名を取得
  return (
    <span
      className={`${colorClasses[color]} text-white px-2 py-1 rounded`}
    >
      {children}
    </span>
  );
};

export default Badge;

このアプローチでは、colorClasses オブジェクト内のクラス名が文字列リテラルとして記述されているため、Tailwind のスキャナーが検出できます。

実装例 2:Alert コンポーネントの修正

より複雑な Alert コンポーネントも同様に対応できます。

完全なクラスマップの定義

typescript// components/Alert.tsx
import React from 'react';

type AlertType = 'success' | 'warning' | 'error' | 'info';

type AlertProps = {
  type: AlertType;
  message: string;
};

// ✓ すべてのクラスを静的に定義
const alertStyles: Record<
  AlertType,
  { bg: string; text: string; border: string }
> = {
  success: {
    bg: 'bg-green-100',
    text: 'text-green-800',
    border: 'border-green-300',
  },
  warning: {
    bg: 'bg-yellow-100',
    text: 'text-yellow-800',
    border: 'border-yellow-300',
  },
  error: {
    bg: 'bg-red-100',
    text: 'text-red-800',
    border: 'border-red-300',
  },
  info: {
    bg: 'bg-blue-100',
    text: 'text-blue-800',
    border: 'border-blue-300',
  },
};

const Alert: React.FC<AlertProps> = ({ type, message }) => {
  const styles = alertStyles[type];

  // ✓ 完全なクラス名を使用
  return (
    <div
      className={`${styles.bg} ${styles.text} ${styles.border} border-l-4 p-4 rounded`}
    >
      <p className='font-semibold'>{message}</p>
    </div>
  );
};

export default Alert;

上記のコードでは、すべてのクラスが静的に定義されているため、safelist なしでも検出されます。

念のための safelist 設定

万全を期すために、safelist も設定しておきましょう。

javascript// tailwind.config.js
module.exports = {
  content: [
    './pages/**/*.{js,ts,jsx,tsx}',
    './components/**/*.{js,ts,jsx,tsx}',
  ],
  safelist: [
    // Alert コンポーネント用(念のため)
    {
      pattern: /bg-(green|yellow|red|blue)-(100)/,
    },
    {
      pattern: /text-(green|yellow|red|blue)-(800)/,
    },
    {
      pattern: /border-(green|yellow|red|blue)-(300)/,
    },
  ],
};

実装例 3:CMS データの動的クラス対応

外部 CMS からクラス名を取得する場合は、safelist が必須です。

CMS から取得するデータ構造

typescript// types/article.ts
export type Article = {
  id: string;
  title: string;
  content: string;
  customStyles: {
    containerClass: string;
    headingClass: string;
    textClass: string;
  };
};

コンポーネントでの使用

typescript// components/CMSArticle.tsx
import React from 'react';
import type { Article } from '@/types/article';

type CMSArticleProps = {
  article: Article;
};

const CMSArticle: React.FC<CMSArticleProps> = ({
  article,
}) => {
  // CMS から取得した動的クラス名を使用
  return (
    <article
      className={article.customStyles.containerClass}
    >
      <h1 className={article.customStyles.headingClass}>
        {article.title}
      </h1>
      <div className={article.customStyles.textClass}>
        {article.content}
      </div>
    </article>
  );
};

export default CMSArticle;

CMS 用の包括的な safelist

javascript// tailwind.config.js
module.exports = {
  content: [
    './pages/**/*.{js,ts,jsx,tsx}',
    './components/**/*.{js,ts,jsx,tsx}',
  ],
  safelist: [
    // すべての色の背景(50〜950)
    {
      pattern:
        /bg-(slate|gray|zinc|neutral|stone|red|orange|amber|yellow|lime|green|emerald|teal|cyan|sky|blue|indigo|violet|purple|fuchsia|pink|rose)-(50|100|200|300|400|500|600|700|800|900|950)/,
    },
    // すべての色のテキスト
    {
      pattern:
        /text-(slate|gray|zinc|neutral|stone|red|orange|amber|yellow|lime|green|emerald|teal|cyan|sky|blue|indigo|violet|purple|fuchsia|pink|rose)-(50|100|200|300|400|500|600|700|800|900|950)/,
    },
    // テキストサイズ全種類
    'text-xs',
    'text-sm',
    'text-base',
    'text-lg',
    'text-xl',
    'text-2xl',
    'text-3xl',
    'text-4xl',
    'text-5xl',
    // パディング(0〜24)
    {
      pattern:
        /(p|px|py|pt|pb|pl|pr)-(0|1|2|3|4|5|6|8|10|12|16|20|24|32)/,
    },
    // マージン(0〜24)
    {
      pattern:
        /(m|mx|my|mt|mb|ml|mr)-(0|1|2|3|4|5|6|8|10|12|16|20|24|32)/,
    },
    // ボーダー・角丸
    {
      pattern: /rounded-(none|sm|md|lg|xl|2xl|3xl|full)/,
    },
    {
      pattern: /border-(0|2|4|8)/,
    },
  ],
};

この設定により、CMS から取得されるほぼすべてのクラスが本番ビルドに含まれます。

デバッグ方法:どのクラスが削除されたか確認する

Tailwind CSS の設定で、削除されたクラスをログ出力できます。

javascript// tailwind.config.js
module.exports = {
  content: [
    './pages/**/*.{js,ts,jsx,tsx}',
    './components/**/*.{js,ts,jsx,tsx}',
  ],
  safelist: [
    // safelist の設定...
  ],
  // デバッグオプション
  future: {
    removeDeprecatedGapUtilities: true,
    purgeLayersByDefault: true,
  },
  // ビルド時の詳細ログ
  corePlugins: {
    preflight: true,
  },
};

また、ビルド時に以下のコマンドでデバッグ情報を出力できます。

bash# ビルド時に詳細なログを出力
NODE_ENV=production npx tailwindcss -i ./styles/globals.css -o ./dist/output.css --verbose

出力された CSS ファイルを確認することで、どのクラスが含まれているか検証できます。

本番ビルドのテスト手順

開発環境と本番環境の差を確認するには、以下の手順でテストします。

ステップ 1:本番ビルドの実行

bash# Next.js の場合
yarn build

# 静的エクスポートの場合
yarn build && yarn export

ステップ 2:本番環境のプレビュー

bash# Next.js の場合
yarn start

# 静的ファイルの場合
npx serve out

ステップ 3:CSS ファイルの確認

ビルドされた CSS ファイルを直接確認します。

bash# ビルドされた CSS を検索
find .next -name "*.css" -type f

# CSS 内のクラスを検索
grep "bg-red-500" .next/static/css/*.css

クラスが見つからない場合は、safelist に追加が必要です。

まとめ

Tailwind CSS でクラスが消える問題は、ツリーシェイクによる最適化が原因です。この問題は、safelist を適切に設定することで確実に解決できます。

本記事でご紹介した対策をまとめます。

#対策方法適用ケースメリット
1safelist で個別指定少数の動的クラスシンプルで確実
2safelist で正規表現パターン多数の関連クラス一括指定が可能
3静的クラスマップコンポーネント内の動的クラススキャナーが検出可能
4CMS 用の包括的 safelist外部データの動的クラスすべてのケースをカバー

重要なポイントは以下の通りです。

動的に生成されるクラス名は、Tailwind のスキャナーが検出できないため、safelist での明示的な指定が必要になります。コンポーネント内で使用するクラスは、可能な限り静的な文字列リテラルとして定義することで、safelist なしでも検出されるようになるでしょう。

CMS や外部 API から取得するクラス名は、包括的な safelist 設定が不可欠です。本番ビルド前に必ずテストを行い、スタイルが正しく適用されているか確認してください。

これらの対策を実践することで、Tailwind CSS のクラスが消える問題を根本から解決できます。安心して動的なスタイリングを実装し、ユーザーに一貫した UI を提供しましょう。

関連リンク