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

Try it yourself
Loading editor…
Press Run to see the result.

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.

Example Β· bash
# 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.gz

Safelist 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.

Example Β· javascript
// 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.

Example Β· css
/* 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

  • Be the first to comment on this lesson.
Performance and Production β€” Tailwind CSS | SoundsCode