Performance & Purging Unused CSS
Understand how Tailwind's JIT engine and content scanning remove unused utility classes from production builds for a tiny final CSS bundle.
Introduction
Tailwind ships with thousands of possible utility classes, covering every combination of color, spacing, and variant. If all of them ended up in your final CSS file, it would be enormous. In practice, production Tailwind bundles are usually just a few kilobytes after compression, because Tailwind only ever generates the CSS for classes it can actually find being used in your project.
- How Tailwind decides which utility classes to generate.
- The role of the content option and why it must be accurate.
- Why dynamically constructed class names silently disappear from production builds.
The Old PurgeCSS Approach
In early versions of Tailwind (v1 and v2), the framework first generated its entire utility set, and a separate tool called PurgeCSS then scanned your files afterward and deleted whatever was not used. It worked, but it was a two-step process: generate everything, then remove most of it, which was slower and occasionally missed classes that PurgeCSS's scanner did not recognize.
The Just-in-Time Engine
Since Tailwind v3, the framework uses a Just-in-Time (JIT) engine by default. Instead of generating everything and purging afterward, it scans your source files first, collects every string that looks like it could be a class name, and generates CSS only for those — nothing more. This is faster, supports arbitrary values like bg-[#1da1f2] (which a purge-based system could never predict in advance), and is always on, with no separate purge step to configure.
The content Option
The content array in tailwind.config.js — the same one you configured for JSX in the previous lesson — is what tells the JIT engine which files to scan for class names.
// tailwind.config.jsmodule.exports = { content: [ './app/**/*.{js,ts,jsx,tsx,mdx}', './components/**/*.{js,ts,jsx,tsx,mdx}', './pages/**/*.{js,ts,jsx,tsx,mdx}', ], theme: { extend: {} }, plugins: [],};If a file is outside these glob patterns, any classes used only in that file are invisible to the scanner — their CSS is never generated, and the styles simply do not apply, with no error or warning.
Why Class Names Must Be Static
The JIT engine scans your files as plain text — it does not execute JavaScript or understand variables. That means it can only see class names that appear complete and literal somewhere in the source.
// ❌ Will NOT work: Tailwind never sees the full class name "text-red-500"function Badge({ color }: { color: string }) { return <span className={`text-${color}-500`}>Badge</span>;}
// ✅ Works: every possible class name appears complete in the sourcefunction Badge({ color }: { color: 'red' | 'green' | 'blue' }) { const colorClasses = { red: 'text-red-500', green: 'text-green-500', blue: 'text-blue-500', }; return <span className={colorClasses[color]}>Badge</span>;}Dynamically built class name strings are the single most common cause of "I added the class but nothing changed" issues in Tailwind projects. The fix is always the same: make sure every full class name appears literally somewhere in a file the content scanner can see.
Measuring the Impact
You can see the effect of content scanning directly by comparing an unused, unminified Tailwind build to what actually ships to production.
Click Run to see what this code prints.
Common Mistakes
- Leaving a folder out of the content array and assuming its styles will "just work" anyway.
- Building class names with string interpolation or concatenation instead of a lookup object of complete class strings.
- Storing Tailwind class names in a database or CMS field, where the scanner has no static file to read them from.
- Assuming JIT purging is something you have to manually enable — in Tailwind v3+ it is the default, always-on behavior, not an opt-in feature.
Best Practices
- Keep the content array as specific as possible while still covering every file that contains class names.
- Use lookup objects (as shown above) instead of template-literal interpolation whenever a class depends on a variable.
- If a class must genuinely come from a dynamic source (like a CMS), maintain a safelist in tailwind.config.js so the scanner knows to always include it.
- Periodically check your production CSS file size as a sanity check — an unexpectedly large bundle usually means content paths are too broad or something is being generated unnecessarily.
Frequently Asked Questions
No. Since Tailwind v3, the JIT engine handles this automatically through content scanning — PurgeCSS is not needed and is not part of a standard setup anymore.
A safelist is a list of class names in tailwind.config.js that should always be generated even if the scanner cannot find them statically — useful when class names genuinely come from a database or external source at runtime.
No, the JIT engine is designed to be fast even on large projects, and incremental rebuilds during development only re-scan what changed.
Key Takeaways
- Tailwind v3+ uses a Just-in-Time engine that generates only the CSS your files actually use.
- The content option tells the scanner which files to read — missing folders mean missing styles.
- The scanner reads files as plain text, so class names must appear complete and literal, never built dynamically.
- A safelist is the escape hatch for the rare case where class names genuinely cannot be static.
Summary
You learned why Tailwind's production CSS bundles stay small despite the framework offering thousands of possible utilities, and why dynamically built class names are the most common source of "missing styles" bugs. Next, you will pull together everything from this course into a set of practical best practices for writing clean, maintainable Tailwind.