Tailwind CSS Performance Optimization
Reduce your Tailwind CSS bundle size with content configuration, purging strategies, production builds, and measurable optimization techniques.
What you'll learn
- ✓How the Tailwind JIT engine generates only the CSS you use
- ✓How to configure the content array to catch every class
- ✓How to audit and reduce your CSS bundle size
- ✓How to avoid common mistakes that bloat your stylesheet
- ✓How to measure the impact of optimizations
Prerequisites
- •A working Tailwind CSS project (v3+)
- •Basic understanding of build tools (Vite, webpack, etc.)
A full Tailwind CSS build with every utility would be over 15MB. In production, a well-configured project ships under 10KB of CSS. The difference is the JIT engine and your content configuration. This article explains how Tailwind keeps your CSS small, what can go wrong, and how to fix it.
How the JIT Engine Works
Since Tailwind v3, the Just-In-Time engine is the default. It scans your source files for class names and generates CSS only for the utilities you actually use. Nothing else ends up in the stylesheet.
Source files (HTML, JSX, Astro, etc.)
|
v
Scanner extracts class names as strings
|
v
Generator creates CSS rules for matched utilities
|
v
Output: tiny CSS file with only used classes
Key: The scanner does NOT parse your code.
It uses regex to find class-like strings.
This is fast but has implications for dynamic classes. The scanner treats your files as plain text. It does not understand JavaScript, JSX, or template logic. It just looks for strings that look like Tailwind classes.
Configuring the Content Array
The content array tells Tailwind which files to scan. Get this wrong and you either ship unused CSS or miss classes entirely.
// tailwind.config.js
export default {
content: [
'./src/**/*.{html,js,jsx,ts,tsx,astro,vue,svelte}',
'./public/index.html',
],
theme: { extend: {} },
plugins: [],
};
Common Mistakes
Missing file extensions. If you use .mdx files with Tailwind classes but do not include *.mdx in the glob, those classes are purged.
// Wrong: misses .mdx and .astro files
content: ['./src/**/*.{html,js,jsx,ts,tsx}'],
// Right: includes all template formats
content: ['./src/**/*.{html,js,jsx,ts,tsx,mdx,astro}'],
Missing directories. If a component library lives in node_modules/@your-org/ui, you need to include it:
content: [
'./src/**/*.{js,jsx,ts,tsx}',
'./node_modules/@your-org/ui/**/*.{js,jsx,ts,tsx}',
],
Overly broad globs. './**/*' scans your entire project, including node_modules, build artifacts, and binary files. This slows builds dramatically. Be specific.
The Dynamic Class Problem
Because the scanner uses regex, it cannot follow dynamic string construction. This is the single most common source of “my class is not working” bugs.
// BROKEN: Scanner cannot find "text-red-500" or "text-green-500"
function Badge({ color }) {
return <span className={`text-${color}-500`}>Status</span>;
}
// WORKS: Scanner finds complete class names as strings
function Badge({ color }) {
const colorMap = {
red: 'text-red-500 bg-red-50',
green: 'text-green-500 bg-green-50',
blue: 'text-blue-500 bg-blue-50',
};
return <span className={colorMap[color]}>Status</span>;
}
The rule is simple: every Tailwind class must appear as a complete, unbroken string somewhere in a scanned file. You cannot concatenate or interpolate parts of a class name.
The Safelist Escape Hatch
If you truly need dynamic classes (for example, from a CMS), use the safelist:
// tailwind.config.js
export default {
safelist: [
'bg-red-500',
'bg-green-500',
'bg-blue-500',
// Pattern matching
{
pattern: /bg-(red|green|blue)-(100|500|900)/,
},
],
};
Use this sparingly. Every safelisted class is included in your CSS regardless of whether it is used.
Measuring Your CSS Bundle
Check the file size
After building, inspect the output CSS:
# Build for production
npm run build
# Check CSS file size
ls -lh dist/assets/*.css
# With gzip estimate
gzip -c dist/assets/style.css | wc -c
A typical Tailwind project should produce:
- Under 30KB raw CSS
- Under 8KB gzipped
If your CSS is over 50KB, something is wrong.
Using the Tailwind CLI
The Tailwind CLI can generate a standalone CSS file for inspection:
npx tailwindcss -o output.css --minify
wc -c output.css
Reducing Bundle Size
1. Remove Unused Plugins
Each plugin adds utilities. If you installed @tailwindcss/typography but do not use prose classes, remove it:
// Only include plugins you actually use
plugins: [
require('@tailwindcss/forms'),
// Remove: require('@tailwindcss/typography'),
],
2. Limit Your Color Palette
Tailwind ships with 22 color families, each with 11 shades. That is 242 potential color values, and each generates dozens of utilities (bg, text, border, ring, etc.). If your design system only uses 5 colors, override the palette:
// tailwind.config.js
export default {
theme: {
colors: {
transparent: 'transparent',
current: 'currentColor',
white: '#ffffff',
black: '#000000',
gray: {
50: '#f9fafb',
100: '#f3f4f6',
200: '#e5e7eb',
// ... only the shades you need
900: '#111827',
},
blue: {
500: '#3b82f6',
600: '#2563eb',
700: '#1d4ed8',
},
red: {
500: '#ef4444',
600: '#dc2626',
},
},
},
};
Note: with JIT mode, unused colors do not generate CSS anyway. But a restricted palette keeps your design consistent and prevents teammates from using arbitrary colors.
3. Disable Core Plugins You Do Not Use
If your project never uses certain utility families:
// tailwind.config.js
export default {
corePlugins: {
float: false, // who uses float in 2026?
clear: false,
skew: false,
sepia: false,
backdropFilter: false,
},
};
4. Use CSS Layers Strategically
Tailwind generates CSS in layers: @layer base, @layer components, @layer utilities. The layer system ensures utility classes always win over component classes in specificity, regardless of source order. Keep your custom CSS in the right layer:
/* globals.css */
@tailwind base;
@tailwind components;
@tailwind utilities;
@layer components {
.btn {
@apply rounded-md px-4 py-2 font-semibold;
}
}
5. Avoid @apply Overuse
@apply copies utility CSS into your component class. Overusing it defeats the purpose of utility-first (and can actually increase bundle size if the same utilities are duplicated across many component classes):
/* Avoid this: duplicates utility CSS for every card variant */
.card { @apply rounded-lg shadow-md p-6 bg-white; }
.card-compact { @apply rounded-lg shadow-md p-3 bg-white; }
.card-dark { @apply rounded-lg shadow-md p-6 bg-gray-800; }
/* Better: use utility classes directly in markup */
/* Or extract a component in your framework (React, Astro, etc.) */
Production Build Checklist
1. Content array covers all template files -> no missing classes
2. No dynamic class concatenation -> no broken utilities
3. Safelist is minimal -> no unnecessary CSS
4. Minification enabled in build tool -> smaller output
5. Gzip or Brotli compression on server -> smaller transfer
6. CSS file size under 30KB raw -> fast first paint
7. No duplicate Tailwind imports -> no doubled output Minification
Most build tools minify CSS automatically in production. If using the Tailwind CLI directly:
npx tailwindcss -i src/input.css -o dist/output.css --minify
Compression
Enable gzip or Brotli on your server or CDN. Most hosting platforms (Vercel, Netlify, Cloudflare) do this automatically. Brotli typically compresses CSS 15-20% better than gzip.
Avoid Duplicate Tailwind Imports
If your CSS has multiple @tailwind utilities directives (perhaps from importing a library’s CSS), you may be generating the utility layer twice. Check your entry CSS file:
/* Should appear only once */
@tailwind base;
@tailwind components;
@tailwind utilities;
Monitoring Over Time
Add a CSS budget to your CI pipeline:
# In your CI script
MAX_CSS_SIZE=30000 # 30KB
CSS_SIZE=$(wc -c < dist/assets/style.css)
if [ "$CSS_SIZE" -gt "$MAX_CSS_SIZE" ]; then
echo "CSS bundle too large: ${CSS_SIZE} bytes (max: ${MAX_CSS_SIZE})"
exit 1
fi
This catches regressions before they ship. If someone adds a safelist pattern that pulls in 500 unused classes, CI will fail.
Tailwind v4 Changes
Tailwind v4 moves configuration into CSS with @theme and automatic content detection. The content array in tailwind.config.js is no longer needed — Tailwind scans your project automatically. But the same principles apply: complete class strings, no dynamic concatenation, and measured output sizes.
/* Tailwind v4 approach */
@import "tailwindcss";
@theme {
--color-brand: #3b82f6;
--color-surface: #f8fafc;
}
Wrap-Up
Tailwind’s JIT engine does most of the optimization work for you. Your job is to configure the content array correctly, avoid dynamic class construction, keep the safelist small, and measure the output. A well-configured Tailwind project ships under 10KB gzipped. If yours is larger, audit your content config, check for duplicate imports, and remove plugins or core features you do not use.
Related articles
- Tailwind Design Tokens and Theming with Tailwind CSS
Build a robust theming system using custom design tokens, CSS variables, theme extension, and multi-brand support in Tailwind CSS.
- Tailwind Responsive Design Patterns with Tailwind CSS
Master mobile-first breakpoints, container queries, responsive grids, and real-world layout patterns using Tailwind CSS utilities.
- Tailwind CSS Grid Layouts with Tailwind CSS
Build powerful grid layouts using Tailwind's grid utilities, auto-fill, auto-fit, spanning, subgrid patterns, and real-world layout examples.
- Tailwind Dark Mode Strategies in Tailwind CSS
Master dark mode in Tailwind CSS with class strategy, media strategy, custom toggles, CSS variable theming, and flash-free server-side approaches.