Production performance and the honest limits

Keep the output small, understand what preflight does to existing markup, and recognise the cases where plain CSS or CSS Modules is the better answer.

Output size

# build, then look at what you actually shipped
npx @tailwindcss/cli -i src/app.css -o dist/app.css --minify
wc -c dist/app.css
gzip -9 -c dist/app.css | wc -c

# find the outliers: a class you did not expect is usually a source you forgot
grep -o '\.[a-z-]*\\[[^]]*\]' dist/app.css | sort | uniq -c | sort -rn | head
FactorEffect on output
PreflightA fixed, small base of normalising rules
Used utilitiesProportional to the classes actually in your source
A wide source globScans tests and fixtures, keeping unused classes alive
@source inline()Every declared name, whether used or not
Arbitrary valuesOne rule each, no reuse
A component library's own CSSOften the largest single contributor
  • The output is a build artefact: compress it, serve it with a long cache lifetime and a content hash, and never inline it into every HTML page.
  • A typical application stylesheet compresses to somewhere in the tens of kilobytes, which is small next to a single unoptimised image.
  • Measure before optimising the framework. In most projects the largest win on a page is an image or a render-blocking third-party script.
💡
The CSS is almost never the performance problem. Check the total transferred bytes per route, the largest contentful paint element, and the number of render-blocking resources before spending an afternoon on stylesheet size - the ordering matters more than the absolute numbers.

Preflight and existing markup

/* preflight removes default margins, unstyled lists and heading sizes */
@import "tailwindcss";

/* scope it away when the page is legacy markup you cannot change */
@import "tailwindcss" source(none);
@source "./app";

/* or restore the browser defaults for one subtree */
@layer base {
  .legacy-content h1 { font-size: 2em; margin: 0.67em 0; }
  .legacy-content ul { list-style: disc; padding-left: 1.5em; }
  .legacy-content button { background: buttonface; border: 1px solid buttontext; }
}
Preflight doesConsequence
Removes margins and heading sizesHeadings look unstyled until you set them
Removes list markers and paddingLists look like plain paragraphs
Sets border-style: solid and width 0A border utility just works
Makes images display: blockNo more inline-image baseline gap
Inherits font from the rootForm controls match the page

Preflight is why a Tailwind page has no unexpected spacing - and also why a page of unstyled HTML rendered inside your layout looks like plain text. Restore the defaults for the subtree you do not control rather than removing preflight globally.

When not to use Tailwind

SituationBetter fit
HTML you do not control (emails, CMS, third-party widgets)Scoped plain CSS
A small site with a dozen rulesPlain CSS in one file
A design system consumed by other teamsCSS variables plus components, Tailwind underneath
Runtime theming per tenantCSS custom properties set on a root element
Rich text from an editorA scoped prose stylesheet, or the typography plugin
Complex state-driven animationWeb Animations API or CSS in a stylesheet
Teams with no build stepA prebuilt stylesheet, or plain CSS
  1. The real cost of utility-first CSS is readability in markup. On a component you own, that cost is low; on someone else's HTML, it is a change you cannot make.
  2. If a class list exceeds roughly a screen, the component is doing too much - that is a design problem, not a framework problem.
  3. Email HTML needs inline styles, so a utility framework that emits a stylesheet is the wrong tool regardless of preference.
  4. Runtime theming is better served by custom properties on a root element than by generating classes per tenant.
  5. Mixing Tailwind with CSS Modules in one project is legitimate: utilities for layout and spacing, modules for the parts with genuinely complex selectors.
  6. Prefer the boring answer. If your team ships faster with plain CSS and it is maintainable, that is the correct choice.
A practical split that works well:

Tailwind      layout, spacing, typography, state variants, one-off adjustments
CSS Modules   complex selectors, keyframe-heavy components, third-party markup
Custom props  anything that must change at runtime or per tenant

FAQ

Is Tailwind's output smaller than my hand-written CSS?
Not necessarily, and it is not the point. The generated file contains only the utilities you used, which is usually small, but a careful hand-written stylesheet for a small site will be smaller. The benefit is that the CSS stops growing as the application does - you add markup, not rules.
How do I keep class lists readable at scale?
Extract a component at the third repetition, declare variants with cva rather than accepting raw class strings, sort with the Prettier plugin so diffs are stable, and use cn() so an override is explicit. None of these reduce the class list; they stop the same list existing twenty times.

Arbitrary values, brackets and the scanning model Component patterns without a component library

Last refreshed 2026-09-18.