Skip to content
Codeloom
Tailwind

Tailwind CSS Performance Optimization

Reduce your Tailwind CSS bundle size with content configuration, purging strategies, production builds, and measurable optimization techniques.

·7 min read · By Codeloom
Intermediate 11 min read

What you'll learn

  • How the Tailwind JIT engine generates only the CSS you use
  • How to configure the content array to catch every class
  • How to audit and reduce your CSS bundle size
  • How to avoid common mistakes that bloat your stylesheet
  • How to measure the impact of optimizations

Prerequisites

  • A working Tailwind CSS project (v3+)
  • Basic understanding of build tools (Vite, webpack, etc.)

A full Tailwind CSS build with every utility would be over 15MB. In production, a well-configured project ships under 10KB of CSS. The difference is the JIT engine and your content configuration. This article explains how Tailwind keeps your CSS small, what can go wrong, and how to fix it.

How the JIT Engine Works

Since Tailwind v3, the Just-In-Time engine is the default. It scans your source files for class names and generates CSS only for the utilities you actually use. Nothing else ends up in the stylesheet.

Source files (HTML, JSX, Astro, etc.)
      |
      v
Scanner extracts class names as strings
      |
      v
Generator creates CSS rules for matched utilities
      |
      v
Output: tiny CSS file with only used classes

Key: The scanner does NOT parse your code.
   It uses regex to find class-like strings.
   This is fast but has implications for dynamic classes.
JIT engine pipeline

The scanner treats your files as plain text. It does not understand JavaScript, JSX, or template logic. It just looks for strings that look like Tailwind classes.

Configuring the Content Array

The content array tells Tailwind which files to scan. Get this wrong and you either ship unused CSS or miss classes entirely.

// tailwind.config.js
export default {
  content: [
    './src/**/*.{html,js,jsx,ts,tsx,astro,vue,svelte}',
    './public/index.html',
  ],
  theme: { extend: {} },
  plugins: [],
};

Common Mistakes

Missing file extensions. If you use .mdx files with Tailwind classes but do not include *.mdx in the glob, those classes are purged.

// Wrong: misses .mdx and .astro files
content: ['./src/**/*.{html,js,jsx,ts,tsx}'],

// Right: includes all template formats
content: ['./src/**/*.{html,js,jsx,ts,tsx,mdx,astro}'],

Missing directories. If a component library lives in node_modules/@your-org/ui, you need to include it:

content: [
  './src/**/*.{js,jsx,ts,tsx}',
  './node_modules/@your-org/ui/**/*.{js,jsx,ts,tsx}',
],

Overly broad globs. './**/*' scans your entire project, including node_modules, build artifacts, and binary files. This slows builds dramatically. Be specific.

The Dynamic Class Problem

Because the scanner uses regex, it cannot follow dynamic string construction. This is the single most common source of “my class is not working” bugs.

// BROKEN: Scanner cannot find "text-red-500" or "text-green-500"
function Badge({ color }) {
  return <span className={`text-${color}-500`}>Status</span>;
}

// WORKS: Scanner finds complete class names as strings
function Badge({ color }) {
  const colorMap = {
    red: 'text-red-500 bg-red-50',
    green: 'text-green-500 bg-green-50',
    blue: 'text-blue-500 bg-blue-50',
  };
  return <span className={colorMap[color]}>Status</span>;
}

The rule is simple: every Tailwind class must appear as a complete, unbroken string somewhere in a scanned file. You cannot concatenate or interpolate parts of a class name.

The Safelist Escape Hatch

If you truly need dynamic classes (for example, from a CMS), use the safelist:

// tailwind.config.js
export default {
  safelist: [
    'bg-red-500',
    'bg-green-500',
    'bg-blue-500',
    // Pattern matching
    {
      pattern: /bg-(red|green|blue)-(100|500|900)/,
    },
  ],
};

Use this sparingly. Every safelisted class is included in your CSS regardless of whether it is used.

Measuring Your CSS Bundle

Check the file size

After building, inspect the output CSS:

# Build for production
npm run build

# Check CSS file size
ls -lh dist/assets/*.css

# With gzip estimate
gzip -c dist/assets/style.css | wc -c

A typical Tailwind project should produce:

  • Under 30KB raw CSS
  • Under 8KB gzipped

If your CSS is over 50KB, something is wrong.

Using the Tailwind CLI

The Tailwind CLI can generate a standalone CSS file for inspection:

npx tailwindcss -o output.css --minify
wc -c output.css

Reducing Bundle Size

1. Remove Unused Plugins

Each plugin adds utilities. If you installed @tailwindcss/typography but do not use prose classes, remove it:

// Only include plugins you actually use
plugins: [
  require('@tailwindcss/forms'),
  // Remove: require('@tailwindcss/typography'),
],

2. Limit Your Color Palette

Tailwind ships with 22 color families, each with 11 shades. That is 242 potential color values, and each generates dozens of utilities (bg, text, border, ring, etc.). If your design system only uses 5 colors, override the palette:

// tailwind.config.js
export default {
  theme: {
    colors: {
      transparent: 'transparent',
      current: 'currentColor',
      white: '#ffffff',
      black: '#000000',
      gray: {
        50: '#f9fafb',
        100: '#f3f4f6',
        200: '#e5e7eb',
        // ... only the shades you need
        900: '#111827',
      },
      blue: {
        500: '#3b82f6',
        600: '#2563eb',
        700: '#1d4ed8',
      },
      red: {
        500: '#ef4444',
        600: '#dc2626',
      },
    },
  },
};

Note: with JIT mode, unused colors do not generate CSS anyway. But a restricted palette keeps your design consistent and prevents teammates from using arbitrary colors.

3. Disable Core Plugins You Do Not Use

If your project never uses certain utility families:

// tailwind.config.js
export default {
  corePlugins: {
    float: false,        // who uses float in 2026?
    clear: false,
    skew: false,
    sepia: false,
    backdropFilter: false,
  },
};

4. Use CSS Layers Strategically

Tailwind generates CSS in layers: @layer base, @layer components, @layer utilities. The layer system ensures utility classes always win over component classes in specificity, regardless of source order. Keep your custom CSS in the right layer:

/* globals.css */
@tailwind base;
@tailwind components;
@tailwind utilities;

@layer components {
  .btn {
    @apply rounded-md px-4 py-2 font-semibold;
  }
}

5. Avoid @apply Overuse

@apply copies utility CSS into your component class. Overusing it defeats the purpose of utility-first (and can actually increase bundle size if the same utilities are duplicated across many component classes):

/* Avoid this: duplicates utility CSS for every card variant */
.card { @apply rounded-lg shadow-md p-6 bg-white; }
.card-compact { @apply rounded-lg shadow-md p-3 bg-white; }
.card-dark { @apply rounded-lg shadow-md p-6 bg-gray-800; }

/* Better: use utility classes directly in markup */
/* Or extract a component in your framework (React, Astro, etc.) */

Production Build Checklist

1. Content array covers all template files    -> no missing classes
2. No dynamic class concatenation              -> no broken utilities
3. Safelist is minimal                         -> no unnecessary CSS
4. Minification enabled in build tool          -> smaller output
5. Gzip or Brotli compression on server        -> smaller transfer
6. CSS file size under 30KB raw                -> fast first paint
7. No duplicate Tailwind imports               -> no doubled output
Production optimization checklist

Minification

Most build tools minify CSS automatically in production. If using the Tailwind CLI directly:

npx tailwindcss -i src/input.css -o dist/output.css --minify

Compression

Enable gzip or Brotli on your server or CDN. Most hosting platforms (Vercel, Netlify, Cloudflare) do this automatically. Brotli typically compresses CSS 15-20% better than gzip.

Avoid Duplicate Tailwind Imports

If your CSS has multiple @tailwind utilities directives (perhaps from importing a library’s CSS), you may be generating the utility layer twice. Check your entry CSS file:

/* Should appear only once */
@tailwind base;
@tailwind components;
@tailwind utilities;

Monitoring Over Time

Add a CSS budget to your CI pipeline:

# In your CI script
MAX_CSS_SIZE=30000  # 30KB
CSS_SIZE=$(wc -c < dist/assets/style.css)

if [ "$CSS_SIZE" -gt "$MAX_CSS_SIZE" ]; then
  echo "CSS bundle too large: ${CSS_SIZE} bytes (max: ${MAX_CSS_SIZE})"
  exit 1
fi

This catches regressions before they ship. If someone adds a safelist pattern that pulls in 500 unused classes, CI will fail.

Tailwind v4 Changes

Tailwind v4 moves configuration into CSS with @theme and automatic content detection. The content array in tailwind.config.js is no longer needed — Tailwind scans your project automatically. But the same principles apply: complete class strings, no dynamic concatenation, and measured output sizes.

/* Tailwind v4 approach */
@import "tailwindcss";

@theme {
  --color-brand: #3b82f6;
  --color-surface: #f8fafc;
}

Wrap-Up

Tailwind’s JIT engine does most of the optimization work for you. Your job is to configure the content array correctly, avoid dynamic class construction, keep the safelist small, and measure the output. A well-configured Tailwind project ships under 10KB gzipped. If yours is larger, audit your content config, check for duplicate imports, and remove plugins or core features you do not use.