Performance & Optimization
Performance & Optimization
Tailwind CSS is designed for performance, but you need to configure it properly to get the smallest possible production builds. Learn how Tailwind purges unused CSS, optimizes output, and best practices for maintaining fast load times.
How Tailwind Purges Unused CSS
Tailwind includes thousands of utility classes by default. In production, most of these classes are never used. Tailwind uses PurgeCSS to scan your files and remove any classes that aren't referenced in your code.
Default vs. Production Build Size
# Development build (all utilities)
Uncompressed: ~3.5 MB
Gzipped: ~400 KB
# Production build (purged + minified)
Uncompressed: ~10-30 KB (typical)
Gzipped: ~5-10 KB (typical)
# Size reduction: ~97-99%
content paths correctly, and Tailwind handles the rest.
Configuring Content Paths
The content option tells Tailwind which files to scan for class names. This is the most critical configuration for optimization:
Content Configuration in tailwind.config.js
// tailwind.config.js
module.exports = {
content: [
// HTML templates
'./index.html',
'./src/**/*.html',
// JavaScript/TypeScript files
'./src/**/*.{js,jsx,ts,tsx}',
// Vue components
'./src/**/*.vue',
// Svelte components
'./src/**/*.svelte',
// PHP templates (Laravel, WordPress, etc.)
'./resources/**/*.blade.php',
'./app/**/*.php',
// Other template engines
'./templates/**/*.twig',
'./views/**/*.ejs',
],
theme: {
extend: {},
},
plugins: [],
}
Content Configuration Best Practices
Follow these patterns for optimal content scanning:
Advanced Content Configuration
// tailwind.config.js
module.exports = {
content: {
files: [
'./src/**/*.{html,js,jsx,ts,tsx,vue}',
'./components/**/*.{js,jsx,ts,tsx}',
'./pages/**/*.{js,jsx,ts,tsx}',
],
// Extract classes from specific patterns
extract: {
// Custom extraction for special syntax
js: {
pattern: /class(?:Name)?=["'`]([^"'`]*)/g,
transform: (match) => match,
},
},
// Transform content before scanning
transform: {
md: (content) => {
return content.replace(/\.md$/, '');
},
},
},
theme: {
extend: {},
},
plugins: [],
}
Production Build Process
Here's how to create optimized production builds with different build tools:
Production Builds with Various Tools
# Vite
NODE_ENV=production npm run build
# or
npm run build # (Vite sets NODE_ENV automatically)
# Webpack
NODE_ENV=production webpack --mode production
# Parcel
parcel build src/index.html
# Next.js
next build
# Create React App
npm run build
# Vue CLI
npm run build
# Laravel Mix
npm run production
process.env.NODE_ENV === 'production' to enable purging and minification. Always ensure your build tool sets this correctly.
Understanding the Build Pipeline
Tailwind's production build goes through several optimization stages:
Build Pipeline Stages
1. CONTENT SCANNING
↓
Scans all files in content paths
Extracts class names using regex patterns
Builds a list of used classes
2. CSS GENERATION
↓
Generates CSS only for used classes
Removes unused utilities, components, base styles
Applies theme customizations
3. PURGING
↓
Removes unused CSS rules
Keeps only classes found in content files
Preserves safelist classes
4. OPTIMIZATION
↓
Minifies CSS (removes whitespace, comments)
Combines duplicate rules
Optimizes selectors
5. AUTOPREFIXER
↓
Adds vendor prefixes
Targets browsers from browserslist config
Ensures cross-browser compatibility
6. OUTPUT
↓
Final optimized CSS file (~5-30 KB typical)
Analyzing Output Size
Monitor your CSS bundle size to catch optimization issues:
Size Analysis Commands
# Basic file size
ls -lh dist/css/main.css
# Gzipped size (what users actually download)
gzip -c dist/css/main.css | wc -c
# Using build tools
npx vite build --analyze # Vite
webpack-bundle-analyzer dist # Webpack
# Manual analysis with PostCSS
npx tailwindcss -i ./src/input.css -o ./dist/output.css --minify
# View detailed statistics
du -h dist/css/main.css
Avoiding Dynamic Class Generation
One of the biggest optimization pitfalls is dynamically generating class names. This prevents proper purging:
DON'T: Dynamic Class Names (Won't Work)
// ❌ BAD: Tailwind can't detect these
const Button = ({ color }) => {
return (
<button className={`bg-${color}-500 text-white`}>
Click me
</button>
);
};
// ❌ BAD: String interpolation
const Alert = ({ type }) => {
return (
<div className={`border-${type}-300 bg-${type}-50`}>
Alert
</div>
);
};
// ❌ BAD: Computed class names
const sizes = ['sm', 'md', 'lg'];
const Box = ({ size }) => {
return <div className={`p-${sizes[size]}`}>Box</div>;
};
DO: Complete Class Names (Works Correctly)
// ✅ GOOD: Complete class names
const Button = ({ color }) => {
const colors = {
blue: 'bg-blue-500 text-white',
red: 'bg-red-500 text-white',
green: 'bg-green-500 text-white',
};
return (
<button className={colors[color]}>
Click me
</button>
);
};
// ✅ GOOD: Conditional with complete names
const Alert = ({ type }) => {
return (
<div className={
type === 'error' ? 'border-red-300 bg-red-50' :
type === 'warning' ? 'border-yellow-300 bg-yellow-50' :
'border-blue-300 bg-blue-50'
}>
Alert
</div>
);
};
// ✅ GOOD: Object mapping
const Box = ({ size }) => {
const sizeClasses = {
sm: 'p-2',
md: 'p-4',
lg: 'p-8',
};
return <div className={sizeClasses[size]}>Box</div>;
};
Safelisting Classes
If you must use dynamic classes (from a CMS, database, etc.), use the safelist option:
Safelist Configuration
// tailwind.config.js
module.exports = {
content: ['./src/**/*.{html,js}'],
safelist: [
// Specific classes
'bg-red-500',
'text-3xl',
'lg:text-4xl',
// Pattern matching
{
pattern: /bg-(red|green|blue)-(400|500|600)/,
variants: ['hover', 'focus'],
},
// All variants of specific classes
{
pattern: /text-(center|left|right)/,
variants: ['sm', 'md', 'lg', 'xl', '2xl'],
},
// Preserve entire color palettes
{
pattern: /^bg-/,
variants: ['hover'],
},
],
theme: {
extend: {},
},
plugins: [],
}
Tree-Shaking and Dead Code Elimination
Modern bundlers can remove unused code, but you need to write code that's tree-shakeable:
Tree-Shakeable Code Patterns
// ✅ GOOD: Named exports (tree-shakeable)
// utils/buttons.js
export function PrimaryButton() { /*...*/ }
export function SecondaryButton() { /*...*/ }
export function DangerButton() { /*...*/ }
// Only imports what you use
import { PrimaryButton } from './utils/buttons';
// ❌ BAD: Default export with object (not tree-shakeable)
// utils/buttons.js
export default {
PrimaryButton: () => { /*...*/ },
SecondaryButton: () => { /*...*/ },
DangerButton: () => { /*...*/ },
};
// Imports everything
import Buttons from './utils/buttons';
const { PrimaryButton } = Buttons;
CSS File Size Optimization Tips
Additional strategies to keep your CSS small:
Optimization Strategies
1. LIMIT CUSTOM UTILITIES
// ❌ Don't add hundreds of custom utilities
theme: {
extend: {
spacing: {
'13': '3.25rem',
'15': '3.75rem',
'128': '32rem',
'144': '36rem',
// ... 50 more custom values
}
}
}
// ✅ Only add what you actually use
theme: {
extend: {
spacing: {
'128': '32rem',
}
}
}
2. USE PLUGINS SELECTIVELY
// ❌ Don't include all plugins
plugins: [
require('@tailwindcss/forms'),
require('@tailwindcss/typography'),
require('@tailwindcss/aspect-ratio'),
require('@tailwindcss/container-queries'),
// ... more plugins
]
// ✅ Only include plugins you actively use
plugins: [
require('@tailwindcss/forms'),
]
3. DISABLE UNUSED VARIANTS
// ❌ Don't enable variants you don't need
variants: {
extend: {
backgroundColor: ['active', 'group-hover', 'peer-checked'],
}
}
// ✅ Only extend variants when necessary
// Default variants are usually sufficient
4. LIMIT COLOR PALETTE
// ❌ Don't include all shades if you only use a few
colors: {
blue: {
50: '#eff6ff',
100: '#dbeafe',
// ... all 10 shades
900: '#1e3a8a',
}
}
// ✅ Define only the shades you use
colors: {
blue: {
500: '#3b82f6',
600: '#2563eb',
700: '#1d4ed8',
}
}
Lazy Loading Considerations
For large applications, consider code-splitting and lazy loading:
CSS Code Splitting Strategies
// 1. Component-level splitting (React)
const HeavyComponent = lazy(() => import('./HeavyComponent'));
// 2. Route-based splitting (React Router)
const Dashboard = lazy(() => import('./pages/Dashboard'));
const Profile = lazy(() => import('./pages/Profile'));
<Routes>
<Route path="/dashboard" element={
<Suspense fallback={<Loading />}>
<Dashboard />
</Suspense>
} />
</Routes>
// 3. Critical CSS inline, rest async (HTML)
<style>
/* Critical above-the-fold CSS inline */
.hero { /* ... */ }
</style>
<link rel="preload" href="/css/main.css" as="style"
onload="this.onload=null;this.rel='stylesheet'">
// 4. Separate utility CSS from components
// tailwind.config.js
module.exports = {
corePlugins: {
preflight: false, // Disable if you have custom base styles
},
}
Monitoring Build Performance
Track your build times and optimize the slow parts:
Performance Monitoring
# Time your builds
time npm run build
# Vite build with timing
vite build --debug
# Webpack bundle analysis
npm install -D webpack-bundle-analyzer
// webpack.config.js
const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin;
module.exports = {
plugins: [
new BundleAnalyzerPlugin()
]
};
# PostCSS timing
DEBUG=* npx tailwindcss -i input.css -o output.css
# Compare before/after sizes
ls -lh dist/css/*.css | awk '{print $5 " " $9}'
bundlesize or size-limit can fail builds if CSS grows beyond your threshold.
Common Optimization Mistakes
Avoid these frequent performance pitfalls:
Performance Anti-Patterns
// ❌ MISTAKE 1: Not setting NODE_ENV
npm run build # Without NODE_ENV=production
// ❌ MISTAKE 2: Incorrect content paths
content: ['./src/*.js'] // Missing subdirectories
// ❌ MISTAKE 3: Including node_modules
content: ['./node_modules/**/*.js'] // Huge scanning time
// ❌ MISTAKE 4: Dynamic classes everywhere
className={`text-${userColor}-500`} // Won't be purged correctly
// ❌ MISTAKE 5: Too many custom utilities
theme: {
extend: {
// Generating 100+ new utility classes
}
}
// ❌ MISTAKE 6: Not minifying
// Missing postcss-cssnano or build tool minification
// ❌ MISTAKE 7: Loading full CSS in development
// Using production CSS in dev (slow HMR)
Exercise 1: Audit Your Build
Analyze your current Tailwind CSS setup:
- Check your production CSS file size (uncompressed and gzipped)
- Verify content paths include all relevant files
- Look for dynamic class generation patterns in your code
- Time your production build
- Document current bundle sizes as a baseline
Exercise 2: Optimize Content Configuration
Improve your content configuration:
- Review all content paths and remove unnecessary globs
- Add any missing file types or directories
- Test that all classes used in your app appear in the build
- Measure the before/after build size
Exercise 3: Refactor Dynamic Classes
Find and fix dynamic class generation:
- Search your codebase for template literals with Tailwind classes
- Refactor dynamic classes to use object mappings
- Add safelist entries for truly dynamic classes
- Verify all necessary classes still appear in production
- Measure the impact on bundle size