Skip to main content
Back to blog Copy Markdown
· 8 min read ·
tailwindcss css frontend

Tailwind CSS 4: A Rust Engine, Rebuilds in Microseconds

Tailwind CSS 4 rebuilt the engine in Rust: ~5x faster full builds, 100x+ faster rebuilds. What broke migrating 3 production projects, the setup that works. →

Óscar Gallego

Óscar Gallego

Web Developer

Tailwind CSS 4 performance - Rust engine optimization illustration
On this page

Tailwind CSS 4 is not a minor update. It’s a complete rewrite of the engine in Rust, and it breaks v3 compatibility in places that will each cost you an afternoon.

I migrated 3 production projects. The speed is real. So is the breakage.

Here’s what nobody tells you.

What v4 changes, in 30 seconds

The framework was rebuilt around a new Rust-powered compiler. Tailwind’s own benchmarks put full builds around 5x faster (378ms to 100ms in their measurement) and incremental rebuilds that compile no new CSS over 100x faster, dropping into microseconds. On top of that, a “CSS-first” configuration that removes the need for a tailwind.config.js file.

The pieces that matter:

  • New Rust engine: roughly 5x faster full builds and incremental rebuilds that finish in microseconds (100x+ faster) thanks to native parallelization.
  • CSS-based configuration: tailwind.config.js is no longer required. CSS-first is the default now, via native CSS variables and the @theme directive inside your main CSS file. A JS config still works if you load it explicitly with @config "./tailwind.config.js".
  • Native Vite integration: it plugs in directly as a Vite plugin (@tailwindcss/vite), no more framework-specific wrappers like @astrojs/tailwind.
  • CSS plugins: now imported straight into the CSS with the @plugin directive.
  • Container queries in core: what used to need a plugin is now built in.

8.5 seconds to 890ms, same project

Before Tailwind v4, my project with ~500 components compiled in 8.5 seconds. With v4: 890ms.

The benchmarks

MetricTailwind v3Tailwind v4Improvement
Initial build8.5s0.89s9.5x faster
Rebuild (HMR)420ms45ms9.3x faster
Output size12.4KB11.1KB10% smaller

Why so fast? Rust enables true parallelization. v4 processes files across multiple threads while v3 sits in Node.js’s single thread, waiting.

tailwind.config.js is no longer required

The most radical change is that the JavaScript configuration file is no longer required. CSS-first is the default, and if you still want a JS config you load it explicitly with @config "./tailwind.config.js". It doesn’t get picked up automatically anymore.

Before (v3)

// tailwind.config.js
module.exports = {
  theme: {
    extend: {
      colors: {
        primary: '#3b82f6',
        secondary: '#8b5cf6',
      },
      fontFamily: {
        sans: ['Inter', 'system-ui', 'sans-serif'],
      },
      spacing: {
        '128': '32rem',
      }
    }
  },
  plugins: [
    require('@tailwindcss/typography'),
    require('@tailwindcss/forms'),
  ],
}

After (v4)

/* src/styles/global.css */
@import "tailwindcss";

@theme {
  /* Custom colors */
  --color-primary: #3b82f6;
  --color-secondary: #8b5cf6;

  /* Fonts */
  --font-sans: 'Inter', system-ui, sans-serif;

  /* Custom spacing */
  --spacing-128: 32rem;

  /* Breakpoints */
  --breakpoint-3xl: 1920px;
}

/* Plugins are now native CSS */
@plugin "@tailwindcss/typography";
@plugin "@tailwindcss/forms";

What you gain:

  1. Improved type-safety: CSS variables get better autocomplete in VSCode.
  2. Instant hot reload: theme changes apply without recompiling.
  3. Less abstraction: what you see in the CSS is what you get.

The correct Astro setup (and the one that breaks)

One thing before anything else: do not use @astrojs/tailwind with Tailwind v4. It causes conflicts.

Setup for Astro + Tailwind v4

npm install tailwindcss @tailwindcss/vite
// astro.config.mjs
import { defineConfig } from 'astro/config';
import tailwindcss from '@tailwindcss/vite';

export default defineConfig({
  vite: {
    plugins: [tailwindcss()],
  },
});
/* src/styles/global.css */
@import "tailwindcss";
---
// src/layouts/Layout.astro
import '@/styles/global.css';
---

And the version that breaks:

// WRONG
import tailwind from '@astrojs/tailwind';

export default defineConfig({
  integrations: [tailwind()], // This breaks Tailwind v4
});

@apply inside @keyframes: the change that hurts most

The most painful change in v4: @apply does not work inside @keyframes.

The case that fails

/* WRONG: this BREAKS in Tailwind v4 */
@keyframes fade-in {
  from {
    @apply opacity-0 scale-95;
  }
  to {
    @apply opacity-100 scale-100;
  }
}

Error:

@apply is not supported within at-rules like @keyframes

The fix: vanilla CSS

/* CORRECT: use standard CSS in keyframes */
@keyframes fade-in {
  from {
    opacity: 0;
    scale: 0.95;
  }
  to {
    opacity: 1;
    scale: 1;
  }
}

/* Then apply the animation */
.fade-enter {
  animation: fade-in 0.3s ease-out;
}

The other trap: @apply with dark mode in Astro

<style>
  /* WRONG: this may fail in some cases */
  .card {
    @apply bg-white dark:bg-gray-900;
  }
</style>

Error:

The `dark:bg-gray-900` class does not exist

The fix is CSS variables plus regular classes:

<div class="card bg-white dark:bg-gray-900">
  <!-- content -->
</div>

<style>
  /* Only styles that don't depend on Tailwind */
  .card {
    border-radius: 0.5rem;
    box-shadow: 0 1px 3px rgba(0,0,0,0.1);
  }
</style>

Five problems from migrating for real

1. Outdated plugins

Many v3 plugins simply don’t work in v4.

Temporary fix:

// Use the v4-compatible version
npm install @tailwindcss/typography @tailwindcss/forms

2. Custom utilities with addUtilities()

Before (v3):

// tailwind.config.js
const plugin = require('tailwindcss/plugin');

module.exports = {
  plugins: [
    plugin(({ addUtilities }) => {
      addUtilities({
        '.scrollbar-hide': {
          '-ms-overflow-style': 'none',
          'scrollbar-width': 'none',
          '&::-webkit-scrollbar': {
            display: 'none'
          }
        }
      });
    })
  ]
}

After (v4):

/* src/styles/global.css */
@layer utilities {
  .scrollbar-hide {
    -ms-overflow-style: none;
    scrollbar-width: none;

    &::-webkit-scrollbar {
      display: none;
    }
  }
}

3. Arbitrary values

v3:

<div class="w-[calc(100%-2rem)]">

v4: (Works the same, but now with better autocompletion)

<div class="w-[calc(100%-2rem)]">

4. Color opacity syntax

v3:

<div class="bg-blue-500/50">

v4: (No changes, but now more efficient)

<div class="bg-blue-500/50">

5. Container queries

Before, this required a plugin. Now it’s built into v4:

<div class="@container">
  <div class="@md:grid-cols-2">
    <!-- Adapts to container, not viewport -->
  </div>
</div>

Dark mode without the flash

Tailwind v4 improves support for class-based dark mode.

The configuration I recommend

@import "tailwindcss";

@variant dark (&:where(.dark, .dark *));

This allows:

<!-- Option 1: Class on root -->
<html class="dark">
  <div class="bg-white dark:bg-gray-900">
</html>

<!-- Option 2: Class on specific container -->
<div class="dark">
  <p class="text-gray-900 dark:text-white">
</div>

Avoid FOUC (Flash of Unstyled Content)

<script>
  // Execute BEFORE render
  if (localStorage.theme === 'dark' ||
      (!localStorage.theme && window.matchMedia('(prefers-color-scheme: dark)').matches)) {
    document.documentElement.classList.add('dark');
  }
</script>

The upgrade tool does half the job

npx @tailwindcss/upgrade

Run it and it converts tailwind.config.js to @theme in your CSS, updates the imports in your files, and detects incompatible plugins. Useful.

What it does not do: migrate custom utilities (that one’s on you) or fix @apply inside keyframes (you rewrite those by hand). Budget time for both.

Production tips that survived the migration

1. Aggressive purge (no longer needed)

In v3, you had to configure purge. In v4, tree-shaking is automatic and smarter. One less thing.

2. Use CSS nesting

/* Take advantage of native CSS nesting */
.card {
  @apply rounded-lg shadow-md;

  &:hover {
    @apply shadow-xl scale-105;
  }

  & .card-title {
    @apply text-xl font-bold;
  }
}

3. Optimize fonts with CSS variables

@theme {
  --font-sans: 'Inter var', system-ui, sans-serif;
  --font-mono: 'JetBrains Mono', monospace;
}

Then use font-variation-settings for dynamic weights:

.dynamic-weight {
  font-variation-settings: 'wght' var(--font-weight);
}

Three migrations, three invoices

Personal portfolio (20 pages)

  • Migration time: 2 hours
  • Build time: 6.2s → 0.78s
  • Problems found: 3 custom plugins (rewritten to CSS)

SaaS dashboard (150 components)

  • Migration time: 1 day
  • Build time: 14.5s → 1.2s
  • Problems found: @apply in keyframes (8 cases), custom animation plugin

E-commerce (300+ components)

  • Migration time: 2 days
  • Build time: 28s → 2.1s
  • Problems found: conflict with @astrojs/tailwind, 15 custom utilities migrated

Is it worth migrating now?

Yes, if:

  • Your build time is >5 seconds
  • You use Vite/Astro (better integration)
  • You’re starting a new project
  • You want to leverage native container queries

Wait, if:

  • You have many incompatible custom plugins
  • Your team can’t dedicate 1-2 days to the migration
  • You rely heavily on @apply in keyframes
  • You use frameworks that don’t officially support v4 yet

Three months in production, and the bill came back positive

The scoreboard after 3 months running v4:

  • Deploy times: reduced by 65%
  • Developer experience: instant HMR (no perceptible lag)
  • Bundle size: 10-15% smaller
  • Bugs: only 2 edge cases with dark mode in Safari

Two Safari edge cases is a bill I’ll happily pay for an 890ms build. The migration hurts at first. Then your rebuilds take 45ms and you stop thinking about your build tool entirely, which is the best compliment a build tool can earn.

If your build takes more than 5 seconds, you already know what to do with your next free afternoon.

Related reading: the setup in this post assumes Astro. If you’re still deciding on the framework itself, start with how Astro got this site down to 18KB of JavaScript.


P.S. Migrating a v3 project and hit something this post doesn’t cover? Tell me on Twitter/X.

Share this article