Tailwind Performance and Best Practices
Tailwind CSS is built to be fast and lean — but only if you use it correctly. A poorly configured project can ship bloated CSS. A well-configured one ships a stylesheet smaller than most images on the page. This topic covers the techniques and habits that keep Tailwind projects fast, readable, and easy to maintain at any scale.
How Tailwind Keeps CSS Small: PurgeCSS and Content Scanning
Tailwind generates only the CSS classes you actually use. It does this by scanning every file listed in your content array during the build process, collecting every class name it finds, and generating CSS only for those classes.
The result: a typical Tailwind production build is between 5 KB and 20 KB of CSS (gzipped). Compare that to a full Bootstrap stylesheet at over 150 KB.
// tailwind.config.js
module.exports = {
content: [
'./src/**/*.{html,js,jsx,ts,tsx}',
'./pages/**/*.{html,js,ts,jsx,tsx}',
'./components/**/*.{html,js,ts,jsx,tsx}',
],
// ...
};Every file type that contains Tailwind class names must appear in the content paths. If a file is missing from this list, its classes will not appear in the final CSS.
Diagram: CSS Output Size Comparison
Development (all classes generated)
├── Full Tailwind CSS .............. ~4 MB
└── Your browser downloads this
|
▼ BUILD STEP (content scan + purge)
Production (only used classes)
├── Your actual used classes ....... ~8–20 KB (gzipped)
└── Browser downloads this instead
Bootstrap (no purge) .............. ~150 KB min
Custom CSS (hand-written) .......... varies, usually 30–80 KB
The content scan happens once at build time, not at runtime. The browser never runs this process — it just receives the small, final CSS file.
The Most Important Rule: Never Build Class Names Dynamically
Tailwind scans your source files as plain text. It looks for complete class name strings. If you build a class name by joining string parts, Tailwind does not find it and does not generate its CSS.
Wrong — class name is split across two strings
// Tailwind cannot find 'text-red-500' or 'text-green-500' here
const color = isError ? 'red' : 'green';
<p className={`text-${color}-500`}>Status</p>Correct — use complete class names
// Tailwind finds both complete class names and generates CSS for both
const colorClass = isError ? 'text-red-500' : 'text-green-500';
<p className={colorClass}>Status</p>Keep complete class names intact in your source. Tailwind is a text scanner, not a JavaScript interpreter. It does not evaluate expressions.
Safelist: Forcing Classes Into the Output
Sometimes a class name genuinely comes from an external source — an API response, a database value, or a CMS field. These names never appear in your source files, so Tailwind cannot scan them.
Use the safelist option in tailwind.config.js to force specific classes into the output regardless of whether they appear in your files.
module.exports = {
safelist: [
'bg-red-500',
'bg-green-500',
'bg-blue-500',
'text-white',
// Pattern — include all alert-related classes
{
pattern: /bg-(red|green|blue|yellow)-(100|500|900)/,
},
],
};Keep the safelist as short as possible. Every item you add increases CSS output. Use patterns only when necessary, not as a general shortcut to avoid scanning.
Enable JIT Mode (Just-in-Time)
Tailwind v3 runs in JIT mode by default. JIT generates CSS on demand as you write classes, rather than pre-generating every possible combination. There is nothing to configure — it is always on in v3.
JIT provides three performance benefits:
- Development builds are fast because only used classes generate
- Arbitrary values like
top-[117px]work without any config - Complex variants like
hover:first:text-blue-500work without listing them
If you maintain an older Tailwind v2 project, check your config for mode: 'jit' and add it if missing.
Organize Your Classes: A Consistent Order
A component with 15 utility classes in random order is hard to read and harder to debug. Pick a consistent order and apply it everywhere in your project.
The Tailwind team recommends grouping classes in this order:
Layout → Flexbox/Grid → Spacing → Sizing → Typography → Background → Border → Effects → Transitions
Example — unordered (hard to scan)
<div class="text-white hover:bg-blue-700 p-4 flex bg-blue-600 rounded-lg font-semibold items-center justify-between shadow-md">Example — ordered (easy to scan)
<div class="flex items-center justify-between p-4 rounded-lg bg-blue-600 hover:bg-blue-700 font-semibold text-white shadow-md">The Prettier plugin for Tailwind CSS (prettier-plugin-tailwindcss) sorts classes automatically on every save. Install it once and the entire team uses the same order without thinking about it.
npm install -D prettier prettier-plugin-tailwindcss// prettier.config.js
module.exports = {
plugins: ['prettier-plugin-tailwindcss'],
};Extract Repeated Patterns Into Components, Not @apply
When you repeat the same set of classes across many elements, two approaches exist: the @apply directive or a reusable component/partial.
@apply — use sparingly
/* In your CSS */
.btn-primary {
@apply px-4 py-2 bg-blue-600 text-white rounded font-medium hover:bg-blue-700;
}@apply works, but it reintroduces CSS class names, defeats the inline-class workflow, and makes it harder to understand what a component looks like by reading its HTML. The Tailwind authors themselves recommend avoiding it except in specific cases.
Better: Extract to a component
// In React (or a WordPress template partial)
function Button({ children, onClick }) {
return (
<button
onClick={onClick}
className="px-4 py-2 bg-blue-600 text-white rounded font-medium hover:bg-blue-700"
>
{children}
</button>
);
}The component holds the classes once. Every button across the project references that component. One edit updates every button. This is more maintainable than @apply and keeps the classes visible in one place.
When @apply Is Appropriate
Use @apply when styling HTML you do not control — for example, HTML coming from a Markdown parser, a WordPress content block, or a third-party library where you cannot add className props.
Diagram: @apply vs Component Extraction
┌──────────────────────────────────┐
│ Approach A: @apply │
│ │
│ CSS file: │
│ .btn { @apply px-4 py-2 ... } │
│ │
│ HTML: │
│ <button class="btn"> │
│ │
│ Problem: You must open the CSS │
│ file to see what .btn looks like│
└──────────────────────────────────┘
┌──────────────────────────────────┐
│ Approach B: Component │
│ │
│ Button.jsx: │
│ <button className="px-4 py-2 │
│ bg-blue-600 text-white..."> │
│ │
│ Usage: <Button>Save</Button> │
│ │
│ Benefit: Classes are visible │
│ right inside the component file │
└──────────────────────────────────┘
Minimize Theme Overrides
Every value you add to your theme generates additional CSS. Add only what your project actually uses.
// Avoid: adding large ranges of custom values
extend: {
spacing: {
'13': '3.25rem',
'14': '3.5rem',
'15': '3.75rem',
// ...all the way to 100
}
}
// Better: add only the specific gaps your design requires
extend: {
spacing: {
'18': '4.5rem',
'88': '22rem',
}
}If you find yourself adding many custom spacing values, check whether the default Tailwind scale already covers your design. Most designs fit entirely within Tailwind's built-in scale.
Avoid Inline Style Mixed With Tailwind
Mixing Tailwind classes with inline style attributes is a habit to avoid. Inline styles override Tailwind classes unpredictably and make the component harder to read.
Avoid
<div className="p-4 rounded" style={{ backgroundColor: '#1e3a5f', color: 'white' }}>Prefer
// Add the color to your theme
extend: {
colors: {
navy: { 900: '#1e3a5f' }
}
}
// Then use the theme token
<div className="p-4 rounded bg-navy-900 text-white">If the value is truly a one-off and does not belong in the theme, use an arbitrary value instead:
<div className="p-4 rounded bg-[#1e3a5f] text-white">Responsive Prefix Strategy
Tailwind uses a mobile-first responsive system. Unprefixed classes apply to all screen sizes. Prefixed classes (sm:, md:, lg:) apply at that breakpoint and above.
A common mistake is writing the desktop version first and then trying to override it on mobile. This creates more class clutter and harder-to-predict behavior.
Wrong — desktop first, then overriding on mobile
<div class="flex-row sm:flex-col">Correct — mobile first, then adding desktop layout
<div class="flex-col md:flex-row">Start with the mobile layout (no prefix), then add prefixed classes for larger screens. This is the intended direction for Tailwind's responsive system.
Reduce Specificity Conflicts With Layer Ordering
Tailwind organizes its CSS into three layers: base, components, and utilities. Utilities always override components, which always override base styles. This predictable cascade prevents specificity battles.
When you write custom CSS, place it in the correct layer using the @layer directive:
@layer base {
/* Global resets and element defaults */
body { @apply font-sans; }
}
@layer components {
/* Reusable multi-class patterns */
.prose-custom { @apply text-gray-700 leading-relaxed; }
}
@layer utilities {
/* One-off overrides that should win against everything */
.text-balance { text-wrap: balance; }
}Custom CSS in @layer utilities slots into the same specificity tier as built-in Tailwind utilities. It does not accidentally override component styles or get overridden by them.
Performance: Load Your CSS File Correctly
In production, Tailwind generates a static CSS file. Serve it with:
- Long-term caching headers — the file changes only on rebuild, so it can be cached for months
- Gzip or Brotli compression — Tailwind's already-small output compresses very well
- A CDN — serve the file from a location close to your users
Do not import the full Tailwind CDN script (cdn.tailwindcss.com) in production. The CDN version runs the class generator in the browser, which is slow and sends far more CSS than needed. It is useful only for quick prototypes and demos.
Audit and Remove Unused Safelist Entries
Over time, safelisted classes accumulate in projects. Quarterly, review your safelist and remove any class that your actual content no longer uses. Tools like purgecss --rejected can show you which safelisted classes were not found in any content file.
TypeScript and Tailwind: Type-Safe Class Names
For teams using TypeScript, the tailwind-variants library (an alternative to cva) provides TypeScript autocomplete and type checking for variant class names. Invalid variant keys become compile errors instead of silent visual bugs.
npm install tailwind-variantsimport { tv } from 'tailwind-variants';
const button = tv({
base: 'px-4 py-2 rounded font-medium',
variants: {
color: {
blue: 'bg-blue-600 text-white',
green: 'bg-green-600 text-white',
red: 'bg-red-600 text-white',
},
},
});
// TypeScript error if you pass an invalid color
button({ color: 'purple' }); // ❌ Type error at compile time
button({ color: 'blue' }); // ✅ ValidBest Practices Summary
| Practice | Why It Matters |
|---|---|
List all file paths in content | Ensures used classes generate; missing paths cause missing styles |
| Keep class names complete | Dynamic string construction prevents class scanning |
| Sort classes with Prettier plugin | Consistent order across entire codebase, zero manual effort |
| Extract to components, not @apply | Classes stay visible; components are easier to test and reuse |
| Mobile-first responsive prefixes | Fewer classes; matches Tailwind's intended cascade direction |
| Use @layer for custom CSS | Predictable specificity; no accidental overrides |
| Use arbitrary values, not inline styles | Keeps all styling in class names; easier to scan and override |
| No CDN script in production | CDN runs in browser; static CSS file is always faster |
| Trim the safelist quarterly | Removes dead CSS; keeps output lean |
