Performance and Production
Ship a tiny stylesheet β and avoid the one mistake that silently drops your classes.
Tailwind's production story is genuinely excellent: the v4 engine scans your source, generates only the utilities you actually used, and the final CSS is usually a few kilobytes gzipped — regardless of how large your design system is.
Leave the browser CDN behind for real sites
The Play / browser CDN (what these lessons use) compiles in the visitor's browser on every load. Wonderful for learning, wrong for production. In a real app, use the Vite plugin or the CLI so compilation happens at build time and ships a static file.
The one mistake that bites everyone
The scanner reads your source as plain text. It cannot execute code, so a class name assembled at runtime is invisible to it:
// BROKEN: this class never gets generated
const cls = `bg-${color}-500`;
// SAFE: full class names the scanner can see
const map = { blue: 'bg-blue-500', red: 'bg-red-500' };
const cls = map[color];The short checklist
- Write complete class names in source — never string-concatenate fragments.
- Need truly dynamic classes? Add them to
@source inline(...)/ a safelist. - Build with the Vite/CLI integration; enable minification (frameworks do this by default).
Example
When to use it
- A Next.js app switches from the CDN script to a proper Tailwind build step, dropping the CSS payload from 4 MB to 8 KB gzipped and shaving 300 ms off Largest Contentful Paint.
- A team adds a safelist entry to their Tailwind config for dynamically assembled class names (e.g., bg-{color}-500 strings built at runtime) so they are never purged in production.
- A CI pipeline lints the built CSS bundle size with a size-limit check and fails the build if the Tailwind output grows beyond a threshold, preventing accidental utility sprawl.
More examples
Tailwind CLI production build
The --minify flag enables cssnano; the CLI scans only files it can reach from app.css, so unused utilities never appear in the output.
# Install Tailwind CLI
npm install -D tailwindcss @tailwindcss/cli
# Build minified production CSS (v4 β no config file needed)
npx @tailwindcss/cli -i ./src/app.css -o ./dist/styles.css --minify
# Check output size
wc -c ./dist/styles.css
gzip -k ./dist/styles.css && wc -c ./dist/styles.css.gzSafelist for dynamic class names
safelist prevents the purger from removing classes whose names are assembled at runtime (e.g., `bg-${color}-500`) and would otherwise be invisible to the static scanner.
// tailwind.config.js (v3)
module.exports = {
content: ["./src/**/*.{html,js,jsx,ts,tsx}"],
safelist: [
{ pattern: /^bg-(red|green|cyan|yellow)-500$/ },
{ pattern: /^text-(red|green|cyan|yellow)-700$/ },
"hidden", "block", "opacity-0", "opacity-100",
],
theme: { extend: {} },
plugins: [],
};Detecting source-scan gaps in v4
In Tailwind v4, @source directives tell the engine to scan additional paths (e.g., third-party packages or email templates) that fall outside the default content glob.
/* app.css β Tailwind v4 entry point */
@import "tailwindcss";
/* Explicitly add directories the auto-scanner might miss */
@source "./src/emails/**/*.html";
@source "./node_modules/@company/ui-kit/dist/**/*.js";
/* Safelist a dynamic pattern */
@source unsafe-inline "bg-(red|green|cyan)-500";
Discussion