Tailwind パフォーマンス最適化:JIT・content設定・本番バンドルサイズ管理
Chrome DevToolsを開くと、あの3.5MBのCSSファイルが目に入りました。
リリースしたばかりの新機能ページが、読み込み時間が800msから3.2秒まで伸びていたのです。あれこれ調べた結果、原因はまさにこのCSSファイルにありました——使っていないTailwindクラスがぎっしり詰まっていたのです。
Tailwindは「パフォーマンスに優しい」と謳っているのに、どうして足を引っ張るのか?
後でわかったのは、問題はTailwindではなく設定にあったということです。JITモードの仕組みを理解し、content設定を正しく調整し、さらに本番ビルドで数層の最適化を加えれば、CSSファイルはMB級からKB級まで小さくできます——Netflixのサイトにいたっては、わずか6.5KBしか使っていません。
今日は、これまで踏んできた落とし穴について話していきます。
一、JITモード:Tailwindのパフォーマンス革命
1.1 従来モードの問題
あの夜3.5MBのCSSファイルを見るまで、私のTailwindへの理解は「ユーティリティファーストのCSSフレームワーク」というレベルにとどまっていました。
後で知ったのですが、Tailwind v2以前の「従来モード」は、あらゆるクラス名の組み合わせを事前生成します——すべての色、すべての余白、すべての派生(hover、focus、disabledなど)を含めてです。中程度の複雑さのプロジェクトでも、開発環境のCSSファイルは10MB、ときにはそれ以上に達します。
10MBのCSSファイルは、ローカル開発では大した問題に見えません——帯域は問題にならないからです。しかしブラウザがこれほど大きなスタイルシートを解析するとなると、メモリ使用量もDevToolsの性能も影響を受けます。
私も以前Firefoxでデバッグしていたとき、こんな経験をしました:クラス名を1つ変えるだけでDevToolsが数秒固まり、ページを更新するともっと遅くて、人生を疑うほどでした。
さらに厄介なのは、従来モードでは多くの設定を怖くて変えられないことです。たとえば新しいブレークポイントを追加したり、focus-visibleの派生を有効にしたりしたいとき、頭の中でまず「これでクラスの組み合わせがどれだけ増えるか」を計算しなければなりません。結果として、チームはパフォーマンスと柔軟性のあいだで選択を迫られるのです。
1.2 JITはいったいどう動くのか
JIT(Just-in-Time)モードはTailwind v2.1で導入され、v3+でデフォルト有効になりました。簡単に言えば、必要なぶんだけ生成するということです。
従来モードはビルド時にあらゆるクラス名の組み合わせを事前生成します。たとえ使わなくてもです。JITモードはその逆で——まずテンプレートファイル(HTML、JSX、Vueなど)をスキャンし、実際にどのクラス名が使われているかを見つけ、そのスタイルだけを生成します。
例を挙げましょう。従来モードでは、TailwindはこんなCSSを生成します:
.bg-black { background-color: #000 }
.hover\:bg-black:hover { background-color: #000 }
.focus\:bg-black:focus { background-color: #000 }
.disabled\:bg-black:disabled { background-color: #000 }
/* ... ほかにも数十の派生の組み合わせ */
たとえプロジェクトでbg-blackを1つ使っているだけでも、ほかの数十の派生のスタイルも生成されてしまいます。
JITモードは違います。テンプレートをスキャンして、bg-blackとhover:bg-blackしか使われていないとわかれば、この2つだけを生成します:
.bg-black { background-color: #000 }
.hover\:bg-black:hover { background-color: #000 }
こうすることで、開発環境のCSSファイルは10MB級から一気にKB級まで下がり——しかも本番環境と同じくらい小さくなります。
1.3 JITの実際の効果
初めてプロジェクトでJITモードを有効にしたとき、ページの更新が速すぎてどこか現実離れして感じたのを覚えています——以前は数秒待っていたのが、今やほぼ一瞬です。
ビルド速度:以前は完全ビルドに2〜3分かかっていたのが、今や数秒で終わります。JITはすべてのスタイルを事前生成する必要がなく、テンプレートファイルをスキャンしてクラス名を抽出するだけで済むからです。
開発体験:DevToolsがもう固まりません。以前はクラス名を1つ変えるたびに、ブラウザがあの10MBのスタイルシートを再解析するのを待っていました。今やスタイルシートは数KBで、変更はほぼ即座に反映されます。
任意値のサポート:これは思わぬ嬉しい発見でした。従来モードではtext-[#facc15]のような任意値を使うには、まず設定でsafelistを有効にする必要がありました。JITモードは直接サポートします:
// 設定で事前定義する必要なく、そのまま使える
<h1 class="text-[2.5rem] mt-[1.35rem] text-[#facc15]">
JITですべてがシンプルに
</h1>
動的クラス名:従来モードでは、'text-' + colorのように連結したクラス名は検出できませんでした。JITモードも制約はありますが、safelistと組み合わせれば動的な場面をより柔軟に扱えます。
ここまで話すと、こう思うかもしれません:それでJITモードはどう有効にするの?と。実は今(Tailwind v3+)はデフォルトでJITなので、追加設定は不要です。まだv2を使っているなら、こう有効化できます:
// tailwind.config.js
module.exports = {
mode: 'jit', // v2では手動で有効化が必要
content: ['./src/**/*.{html,js,jsx,ts,tsx}'],
// ...
}
二、content設定:精密スキャンの鍵
JITモードは便利ですが、その前提はcontent設定が正しいことです。
contentは、Tailwindがクラス名を抽出するためにどのファイルをスキャンするかを決めます。設定を誤ると、スタイルが消える(スキャン範囲が狭すぎる)か、CSSが肥大化する(スキャン範囲が広すぎる)かのどちらかになります。
2.1 content設定の基本
基本構文はとてもシンプルです:
// tailwind.config.js
module.exports = {
content: [
'./src/**/*.{html,js,jsx,ts,tsx}', // src配下のすべてのテンプレートファイルをスキャン
],
// ...
}
ここでの**は任意の階層のディレクトリを表し、*.{html,js,jsx,ts,tsx}はこれらの拡張子のファイルにマッチすることを表します。
特定のフレームワークを使っている場合は、パスを調整する必要があるかもしれません:
// Next.jsプロジェクト
content: [
'./pages/**/*.{js,ts,jsx,tsx}',
'./components/**/*.{js,ts,jsx,tsx}',
'./app/**/*.{js,ts,jsx,tsx}', // App Router
]
// Astroプロジェクト
content: [
'./src/**/*.{astro,html,js,jsx,ts,tsx}',
]
ポイントは、Tailwindクラス名を使っているすべてのファイルをカバーすることです。ディレクトリを1つ漏らすと、そのディレクトリ内のスタイルは生成されません。
2.2 私が踏んだ落とし穴
落とし穴1:広すぎるglobパターン
私は当時、設定を「シンプル」にしようとして、こう書きました:
content: [
'./**/*.js', // これはnode_modulesまでスキャンする!
]
その結果、Tailwindはnode_modules内のすべてのJSファイルまでスキャンしてしまい、ビルド時間が跳ね上がり、わけのわからないスタイルが大量に生成されました。
正しいやり方は範囲を制限することです:
content: [
'./src/**/*.js', // srcディレクトリだけスキャン
'./components/**/*.js', // componentsディレクトリだけスキャン
]
落とし穴2:コンポーネントディレクトリの漏れ
あるときプロジェクトをリファクタリングして、コンポーネントを./lib/components/ディレクトリに移しました。content設定の更新を忘れた結果、新しい場所のコンポーネントのスタイルがすべて消えました。
半日かけて原因を突き止めました。教訓はこうです:プロジェクト構造を変えるときは、必ずcontent設定も同期する。
落とし穴3:動的に組み立てたクラス名
こんな書き方です:
const color = 'red';
const className = `text-${color}-500`; // JITは検出できない
JITがスキャンするときに見えるのはテンプレート文字列であり、完全なクラス名ではありません。その結果、本番ビルド時にこのスタイルは削除されます。
解決策はsafelistを使う(後で説明します)か、オブジェクト構文に変えることです:
const colors = {
red: 'text-red-500',
blue: 'text-blue-500',
};
const className = colors[color]; // 完全なクラス名なので検出できる
2.3 safelistと動的クラス名
どうしても動的クラス名が必要な場面もあり、そんなときにsafelistが役立ちます。
// tailwind.config.js
module.exports = {
safelist: [
'text-red-500',
'text-blue-500',
'bg-red-500',
// または正規表現で一群のクラス名をマッチさせる
{
pattern: /text-(red|blue|green)-(500|600)/,
variants: ['hover', 'focus'], // 派生も同時に保持
},
],
}
safelistは、テンプレートファイルに直接現れなくても、これらのスタイルをTailwindに強制生成させます。
ただし注意してください:safelistが多いほど、CSSファイルは大きくなります。必要な場面だけで使い、「保険箱」のように使う可能性のあるクラス名をすべて詰め込むのはやめましょう。
三、本番バンドルサイズ管理:4層最適化戦略
JITモードのおかげで開発環境の小さなCSSが可能になりますが、本番ビルドにはさらなる最適化が必要です。
私は設定から圧縮まで段階的に進む、4層の最適化戦略を整理しました。
3.1 第1層:精密なcontent設定
これが最も基礎的なもので、前述のとおりです。
核心原則は、Tailwindクラス名を実際に使っているファイルだけをスキャンすること。範囲が精密なほど、ビルドは速く、CSSは小さくなります。
// 良い:精密な範囲
content: [
'./src/components/**/*.jsx',
'./src/pages/**/*.tsx',
]
// 悪い:範囲が広すぎる
content: [
'./**/*.js', // node_modulesまでスキャンしてしまう
]
3.2 第2層:PurgeCSSによる自動除去
Tailwind v3+は本番ビルド時にPurgeCSSを自動で有効にし、未使用のスタイルを除去します。
重要なのは、ビルドコマンドが開発/本番環境を正しく区別していることです:
# 開発ビルド(未使用スタイルは除去しない)
npm run dev
# 本番ビルド(PurgeCSSを自動適用)
npm run build
PostCSSを使っているなら、設定で明示的に制御できます:
// postcss.config.js
module.exports = {
plugins: {
tailwindcss: {},
autoprefixer: {},
...(process.env.NODE_ENV === 'production' ? { cssnano: {} } : {}),
},
}
3.3 第3層:cssnanoによるMinify
PurgeCSSが未使用スタイルを除去した後、cssnanoがCSSをさらに圧縮します。
圧縮には次が含まれます:コメントの削除、重複ルールのマージ、セレクタの簡略化、数値の圧縮など。
# Tailwind CLIで直接minify
npx tailwindcss -i ./src/input.css -o ./dist/output.css --minify
# またはPostCSS経由(上で示したとおり)
実測データを1つ:私のプロジェクトは150KB(PurgeCSS後)から45KB(minify後)まで圧縮できました。
3.4 第4層:Brotli/Gzipによるネットワーク圧縮
この層はCSSそのものの最適化ではなく、ネットワーク転送の層です。
サーバーがBrotliまたはGzipで静的リソースを圧縮すれば、CSSファイルをさらに60〜80%小さくできます。
# NginxでBrotliを設定(ngx_brotliモジュールのインストールが必要)
brotli on;
brotli_comp_level 6;
brotli_types text/css application/javascript;
# またはGzipを使う(Nginxはデフォルトでサポート)
gzip on;
gzip_comp_level 6;
gzip_types text/css application/javascript;
比較データ:45KBのCSSファイルは、Brotli圧縮後の転送サイズが約8KBになります。
だからNetflixが「うちのCSSは6.5KBしかない」と言っているのは、Brotli圧縮後の転送サイズを指しているのであって、元のファイルサイズではありません。
四、実践事例とデータ比較
4.1 私の最適化前後の比較
正直に言うと、このデータ比較を見たとき、自分でもかなり驚きました。
4.2 よくある問題のトラブルシューティング
スタイルが消えたらどうする?
第1ステップ:content設定を確認し、Tailwindクラス名を使っているすべてのファイルがスキャン範囲に入っているか確かめます。
第2ステップ:動的クラス名がある場合、safelistが必要かどうか確認します。
第3ステップ:ビルドコマンドを確認し、開発ビルドではなく本番ビルド(npm run build)を使っているか確かめます。
CSSがまだ大きいときは?
safelistが多すぎないか確認します。サードパーティライブラリのCSSがTailwindのビルド成果物に混入していないか確認します。content範囲が広すぎないか確認します。
DevToolsが固まるときは?
Tailwindのバージョンが3以上(JITがデフォルト有効)か確認します。content設定が精密か確認します。まだv2を使っているなら、アップグレードするか手動でJITモードを有効にします。
五、Tailwind v4の新機能展望
既存の最適化を語り終えたところで、Tailwind v4(2024年末リリース)の新しい変化も見てみましょう。
5.1 Oxideエンジン
これが最も心躍るアップデートです:TailwindがRustで基盤エンジンを書き直しました。
公式データでは、インクリメンタルビルドの速度が182倍向上しました。要するに、以前はクラス名を1つ変えると数秒待って再コンパイルしていたのが、今やほぼミリ秒級の応答になりました。
仕組みは、Oxideエンジンがファイルスキャン結果をキャッシュし、変更された部分だけを再処理するというものです。これは大型プロジェクトで特に有効で——私たちのあるプロジェクトは200以上のコンポーネントファイルがあり、以前は毎回のビルドに10秒ほどかかっていたのが、今やほぼ秒級で完了します。
5.2 ゼロ設定という目標
Tailwind v4は新しい理念を押し出しています:ほとんどのプロジェクトではtailwind.config.jsが不要になる、というものです。
デフォルト設定がすでに一般的なニーズをカバーしています:モダンなCSS機能、コンテナクエリ、合理的なブレークポイント、完全なカラーシステム。深いカスタマイズが必要なときだけ設定ファイルを書きます。
つまり新規プロジェクトの立ち上げがより速くなり、設定ミスの機会も少なくなるということです。
ただしv4への移行では注意が必要です:設定構文が変わっています。たとえば以前のtheme.extend.colorsは、今やCSSカスタムプロパティで書く必要があります。移行前に公式のアップグレードガイドに一度目を通しておくとよいでしょう。
まとめ
あの深夜3時の崩壊のあと、私はプロジェクトのTailwind設定を一から見直しました。
JITモードのおかげで開発も本番も同じ小さなCSSを使え、content設定がスキャン範囲を決め、4層最適化でCSSをMBからKBまで圧縮できます。さらにTailwind v4のOxideエンジンが加わり、ビルド速度はもうボトルネックではなくなりました。
あなたのプロジェクトがまだ従来モードを使っていたり、content設定に不安があったりするなら、まず次の点を確認してみてください:
- Tailwindのバージョンが3以上か(JITがデフォルト有効)
- content設定がすべてのテンプレートファイルを精密にカバーしているか
- 本番ビルドでPurgeCSSとcssnanoが有効になっているか
- サーバーでBrotli/Gzip圧縮を設定しているか
これらを直せば、高い確率で明らかなパフォーマンス向上が見られるはずです。
質問があればいつでもコメントしてください。Tailwind公式ドキュメントのJITと本番最適化の章も、わかりやすく書かれているのでおすすめです。
参考資料
- Tailwind CSS Just-in-Time Mode ドキュメント
- Optimizing for Production ドキュメント
- Just-In-Time: The Next Generation of Tailwind CSS
- Tailwind CSS v4.0 リリース告知
- Tailwind CSS Best Practices for Performance Optimization
Tailwind CSS パフォーマンス最適化設定
JITモードの有効化から本番バンドルサイズ管理までの完全な設定フロー
⏱️ 目安時間: 30 分
- 1
ステップ1: Tailwindのバージョンを確認する
プロジェクトのTailwind CSSバージョンを確認します:
• Tailwind v3+はJITモードがデフォルトで有効
• Tailwind v2は設定にmode: 'jit'を手動で追加する必要がある
• 最良の性能のためv3+へのアップグレードを推奨 - 2
ステップ2: contentのスキャンパスを設定する
tailwind.config.jsで精密なスキャンパスを設定します:
• Next.js: ['./pages/**/*.{js,ts,jsx,tsx}', './components/**/*.{js,ts,jsx,tsx}']
• Astro: ['./src/**/*.{astro,html,js,jsx,ts,tsx}']
• './**/*.js'のような広すぎるglobパターンは避ける - 3
ステップ3: 動的クラス名を処理する
動的に組み立てるクラス名は、safelistで強制的に生成します:
• safelist配列に完全なクラス名を追加する
• patternの正規表現で一群のクラス名をマッチさせる
• variantsを併用してhoverやfocusなどの派生も保持する - 4
ステップ4: 本番ビルドの最適化を設定する
PostCSS設定にcssnanoを追加します:
• 開発環境ではcssnanoを有効にしない(ビルドが速くなる)
• 本番環境ではcssnanoのminifyを自動で有効にする
• process.env.NODE_ENVで有効化条件を制御する - 5
ステップ5: サーバー側の圧縮を設定する
NginxでBrotliまたはGzip圧縮を設定します:
• Brotli: brotli on; brotli_comp_level 6;
• Gzip: gzip on; gzip_comp_level 6;
• いずれもtext/css application/javascriptタイプを設定する
FAQ
Tailwind v3でJITモードはどう有効化しますか?
content設定でファイルを漏らすとどんな問題が起きますか?
動的クラス名はなぜJITに見逃されるのですか?
Netflixの6.5KB CSSは元のサイズですか、圧縮後ですか?
Tailwind v4のOxideエンジンには何が改善されていますか?
safelistの設定が多すぎると何に影響しますか?
5分で読めます · 公開日: 2026年3月30日 · 更新日: 2026年6月8日
Tailwind と shadcn/ui 実践ガイド
検索からこのページに来た場合は、前後の記事もあわせて読むと同じテーマの理解がかなり早く深まります。
前の記事
Dialog、Sheet、Popover:オーバーレイ系コンポーネントのアクセシビリティとフォーカス管理
shadcn/ui の Dialog、Sheet、Popover という 3 種類のオーバーレイコンポーネントについて、アクセシビリティの実装とフォーカス管理を深掘り。WCAG 標準、ARIA 属性、キーボードナビゲーション、フォーカストラップを扱い、完全なコード例を提供します
第 7 / 11 記事
次の記事
Astro + Tailwind:アイランドコンポーネントとグローバルスタイルを衝突させない設定
Astro のアイランドアーキテクチャで Tailwind CSS のスタイルが衝突したらどうする?astro-island/astro-slot の仕組み、Tailwind v4 の正しい統合方法、よくある 4 つのスタイル衝突シーンの解決策を解説し、実戦で落とし穴を回避します
第 9 / 11 記事
関連記事
Tailwind v4 + Vite:5 分で完成する設定テンプレートとディレクトリ構成
Tailwind v4 + Vite:5 分で完成する設定テンプレートとディレクトリ構成
shadcn/ui のインストールとテーマカスタマイズ完全ガイド(CSS 変数つき)
shadcn/ui のインストールとテーマカスタマイズ完全ガイド(CSS 変数つき)
shadcn/ui で管理画面の骨組みを構築:Sidebar + Layout ベストプラクティス
コメント
GitHubアカウントでログインしてコメントできます