TypeScript

Fix slow Tailwind v3 in Vite: remove catch-all safelists

Separate oversized generated CSS from plugin cost, with a single-change compiler reproduction and an Autoprefixer negative control.

Mahdi Ben RhoumaMahdi Ben Rhouma
8 min read

TL;DR#

If a Tailwind v3 project has a catch-all safelist, remove that pattern before changing unrelated Vite settings. It requests utilities that your source files never use. Keep genuinely external class choices as a short explicit list or a bounded pattern, then compare generated CSS and the actual update delay. Retain Autoprefixer for the first comparison so the experiment changes only one input.

The Vite and React slow-compilation report describes slow updates and an unresponsive browser Styles pane. Its Tailwind configuration includes:

js
module.exports = {
  content: ['./app/**/*.{html,js,jsx,ts,tsx}'],
  safelist: [{ pattern: /./ }],
  darkMode: 'class',
  theme: { extend: {} },
  plugins: [require('@tailwindcss/forms')],
}

The answers suggest removing Autoprefixer or narrowing the content paths. Neither answer proves which operation consumed the original project's time. The broad safelist is a separate, directly testable source of excess generated CSS. This article reproduces that generation behaviour; it does not claim to have run the author's application or measured its browser delay.

Why it happens#

The Tailwind v3 safelist documentation describes safelisting as a last resort for content unavailable to the scanner. Source scanning and safelisting supply candidates through different routes. A small content list does not place a ceiling on an independent safelist.

The tested compiler implementation walks its configured utility candidates, tests each against a safelist pattern, and adds matching names for generation. A pattern matching any character accepts those non-empty names without asking whether the application used them. The effect is not restricted to the folder named in content.

That does not mean every imaginable CSS value exists in the output. The candidate set comes from configured utilities and theme values. Arbitrary values are not an infinite catalogue for the regex to enumerate. Similarly, state and responsive variants are requested separately; a broad base pattern is not a precise description of the combinations your interface supports.

Fix#

1. Confirm the Tailwind generation before editing its configuration#

Run this in the application workspace:

sh
npm ls tailwindcss autoprefixer

This diagnosis concerns the JavaScript safelist supported by Tailwind v3. If the project now runs v4, use the v4 safelist migration diagnosis instead. Loading a legacy config and supporting its safelist are separate questions. Do not attempt to repair missing v4 classes by increasing an old v3 regex.

Find the config the application actually uses. Check whether the catch-all pattern is present directly or supplied through a shared configuration. Preserve the existing theme, plugins and PostCSS setup during the comparison: replacing the whole config could remove the excess output while also breaking the application's design system.

2. Remove the catch-all request, keeping the rest of the pipeline#

For the minimal configuration above, the corresponding change is:

js
module.exports = {
  content: ['./app/**/*.{html,js,jsx,ts,tsx}'],
  safelist: [],
  darkMode: 'class',
  theme: { extend: {} },
  plugins: [require('@tailwindcss/forms')],
}

Use the source paths matching your own application; this example follows the question's folder. A Vite project keeping components elsewhere needs those files scanned as well. The goal is not to exclude real component files to obtain a smaller number, but to stop requesting utilities independently of any real use.

Keep the v3 PostCSS integration while testing this change:

js
module.exports = {
  plugins: {
    tailwindcss: {},
    autoprefixer: {},
  },
}

Tailwind's official v3 Vite installation includes both plugins. Removing one can change the pipeline's cost, but it does not remove the broad request made inside Tailwind. Establish whether the stylesheet was oversized before deciding that a dependency must go.

3. Restore only classes that genuinely arrive from outside the source#

An administrative interface might accept a bounded choice from stored content rather than contain the complete token in a scanned component. For that situation, an explicit class list is easier to inspect than a catch-all. An alternative minimal configuration is:

js
module.exports = {
  content: ['./app/**/*.{html,js,jsx,ts,tsx}'],
  safelist: ['bg-green-500'],
  darkMode: 'class',
  theme: { extend: {} },
  plugins: [require('@tailwindcss/forms')],
}

Use the actual supported external choices. If component code already selects among complete literal strings, let content scanning find those strings instead. If it assembles a class from fragments, replace that construction with a mapping of complete tokens rather than generating a whole design system to hide the scanning gap.

When a regex is appropriate, constrain its utility family and permitted values. The v3 documentation also explains that patterns target base utilities and that requested variants belong in the variants option. A pattern matching everything plus an expanding variant list makes the requested output broader again; it does not make the scanner understand runtime JavaScript.

Verify the fix#

The reproduction ran on 3 November 2026 in an isolated Node 24.22.2 container with Tailwind CSS 3.4.19, PostCSS 8.4.49, Autoprefixer 10.4.21 and the forms plugin. To keep it small, its configuration used a fixed palette and a fixed subset of core utilities throughout. Its source contained only flex, padding and text-colour utilities. It tested compilation and selector presence, without running a Vite server or browser rendering benchmark.

Each scenario ran in a fresh process. The results were:

Safelist and pipelineCSS bytesRule count
Catch-all, Autoprefixer retained22013468
Empty safelist, Autoprefixer retained923
Explicit external background class, Autoprefixer retained1404
Catch-all, only Autoprefixer removed22013468

Those figures belong to the deliberately small fixture, not a forecast for your application. The important controls are what changed and what remained: removing only the safelist retained the literal padding utility and removed an unrelated red background utility. The bounded list restored the intended green background without restoring the red one. Removing only Autoprefixer left the broad output unchanged in this fixture.

In your project, compare the emitted or served stylesheet before and after the safelist edit. Inspect representative selectors your components need, including externally supplied choices. Then measure the same development update again. A smaller stylesheet demonstrates less generated output; it does not by itself prove which part of the original HMR path was slow.

One email a month — the latest explained errors

What the site published that month — mostly Next.js, Supabase and Node errors, explained. You confirm by email; unsubscribe any time in one click.

Distinguish a smaller stylesheet from a solved browser stall#

If the stylesheet is now compact but updates remain slow, investigate the remaining stage. Vite's performance guide documents plugin-transform diagnostics and CPU profiling. Use that evidence to identify expensive plugin work instead of attributing every delay to the first plugin listed in a config. Repeat comparable operations when measuring; a first startup and an already warm update answer different questions.

A different symptom needs its own investigation. If built selectors exist but production looks unstyled, check whether Vite's built CSS is actually being loaded. If Chrome on Linux stalls with many module requests pending while another browser loads the app, the Vite browser file-descriptor case concerns request handling rather than a safelist.

Keep the supported class vocabulary explicit and check both output coverage and the measured delay. That preserves the utilities the application actually needs while giving the next performance investigation a smaller, understood stylesheet to work with.

Frequently Asked Questions

|

Have more questions? Contact us

Still stuck? Paste your error.

Paste the error message or stack trace — we check it against every fix and tool on the site.

Matching happens in your browser. When you search, a cleaned error signature is sent so we can spot errors we do not cover yet — never the raw text, your IP address or your e-mail. Do not paste passwords or keys. Details

Written by

Mahdi Ben Rhouma
Mahdi Ben Rhouma

Full-Stack Dev — Next.js & Supabase

Solo developer building SaaS products with Next.js and Supabase. Writing about production patterns the official docs skip.

Remote