Tooling and Maintenance
Tailwind CSS Intermediate/Advanced
Chapter 7 ยท Performance, Tooling and Maintenance
A Tailwind project is easy to start and, after a year, easy to let go untidy: class lists in different orders, typos that silently do nothing, conflicting classes, a stylesheet nobody has looked at. This chapter measures the things people worry about (output size, build time), then sets up the tools that keep a project tidy (Prettier, ESLint, editor settings), and finishes with a way to catch visual changes and a review checklist.
What Decides the Size of the Output
The generated CSS contains one rule per distinct class that the scanner found (chapter 1), plus the reset and the theme variables you use. It does not grow with the number of files or with how often a class repeats. I generated three projects of 500 files each, with 30 random classes per file, drawn from pools of different sizes, and built each from inside its own folder:
| Project | Distinct classes | Plain | --minify | Minified + gzip |
|---|---|---|---|---|
| small pool | 150 | 18,992 B | 13,605 B | 3,072 B |
| colours, spacing, sizes | 785 | 53,825 B | 41,673 B | 7,050 B |
the same 785 with 5 variant prefixes (hover:, md:, dark:, focus:, lg:hover:) | 4,513 | 326,471 B | 239,489 B | 29,008 B |
Size follows the number of distinct class-plus-variant combinations. Every extra variant on a class is another rule. A page that is 30 KB gzipped after minifying has thousands of distinct combinations, not thousands of elements. Separately, three settings changed the size of the 150-class project (13,605 B minified, 3,062 B gzipped):
Two honest limits. First, I did not test whether dropping the reset is safe for your pages; the saving (3.6 KB here) is the reset's cost, and the browsers will then apply their own default margins and fonts. Second,
the build already writes only the theme variables you use, so theme(static) is the expensive direction; use it only when something outside Tailwind reads the variables.
Build Time
On the 4,513-class project (500 files) I first timed npx @tailwindcss/cli โฆ and got about 3.5 seconds for every project, whatever its size. That was npx starting up. Calling the program
directly (node node_modules/@tailwindcss/cli/dist/index.mjs) and reading the CLI's own "Done in" line gave a very different picture:
Watch mode printed several "Done" lines for each edit (the file system reports more than one event), so I cannot say which line belongs to which edit; the range is what I can state. Content edits were from under a millisecond to about 80 ms; changing the CSS file took 270 ms, close to a cold build, because the theme may have changed. I did not verify the speed-up figures in Tailwind's release notes, and I did not time bundler integrations (Vite, Next.js). If a build is slow, measure the tool itself with its own timer before blaming Tailwind.
Browser Support Is a Decision
Tailwind's compatibility page says v4 "is designed for and tested on modern browsers" and depends on Chrome 111 (March 2023), Safari 16.4 (March 2023) and Firefox 128 (July 2024). The page does not say which features set those numbers. I built a page with a shadow, a ring, a translate, a rotate, an opacity colour, a hover colour and a backdrop blur and counted what the CSS uses:
That is the list your decision rests on: layers, @property, color-mix() and oklch(). If a real share of your visitors use an older browser, Tailwind v4 is the wrong
choice and staying on v3 is the supported one (chapter 8 covers the migration and when not to). Check your own analytics, not a general rule. Tailwind's page also says the bleeding-edge utilities
(field-sizing, @starting-style, text-wrap: balance) are optional: do not use them if your targets lack them. I did not test any browser except Chrome.
Sorting Classes with Prettier
A class list in a different order on every element makes diffs noisy and reviews slower. prettier-plugin-tailwindcss sorts them into Tailwind's own order. In v4 it needs to know your CSS file, so it can
see custom theme names. My configuration and results:
The order is: classes Tailwind does not know first (custom-thing), then utilities in Tailwind's order, then variants (hover:, md:, dark:) last. Without the
stylesheet setting the plugin did not know bg-brand (a theme name) and put it in the unknown group at the front, so a missing setting changes the sort silently. For helper functions:
Strings inside a className attribute are sorted without any setting, even inside cn(...). Strings in a call that is not inside a class attribute, like the cva table,
need tailwindFunctions.
Linting: Typos, Conflicts and Unknown Classes
A typo like bg-brandd builds without error and styles nothing (chapter 6). A linter can catch it. I ran two ESLint plugins on the same file with a conflict, a typo and an unknown class:
Both work with v4 and both read your CSS file so that bg-brand counts as known (the typo bg-brandd does not). They differ in what they report as an error: the first treats an unknown
class as a warning (it may be your own class), the second as an error. Plugins change faster than Tailwind: eslint-plugin-tailwindcss 4.4.0 exposes its config as configs.recommended
only; the "flat/recommended" name from its README example (written as flat/recommended || recommended) did not exist in this version.
Do the tools agree with each other? I formatted a line with Prettier and linted the result. eslint-plugin-tailwindcss reported nothing. eslint-plugin-better-tailwindcss
reported its order rule satisfied, but its line-wrapping rule wanted the classes split over several lines, which fights Prettier. If you use Prettier, turn that rule off
("better-tailwindcss/enforce-consistent-line-wrapping": "off"; I did not run that exact setting) and let one tool own each job.
Editor settings (VS Code)
Not run here; these come from the Tailwind CSS IntelliSense extension's README. "files.associations": {"*.css": "tailwindcss"} opens CSS files in Tailwind mode. tailwindCSS.classAttributes
(default class, className, ngClass, class:list) and tailwindCSS.classFunctions say where to offer completions; add cn and cva there as well as in
Prettier. tailwindCSS.lint.cssConflict (default warning) flags classes that set the same property, and tailwindCSS.lint.suggestCanonicalClasses flags class names not in their shortest form.
Long Class Lists
A button with fifteen classes repeated on every button is a maintenance problem, not a size problem. I built 200 buttons with the same 15 classes inline, and again with a .btn class made by
@apply inside @layer components:
Repetition compresses almost to nothing, so the transferred difference was 355 bytes (12%) on 200 identical buttons. The real argument for extracting is that you change the button in one place. The options, in the order I would try them:
| Option | Use when | Cost |
|---|---|---|
| A component (React, Vue, Astro, a template partial) | the same markup repeats | none beyond the component; the classes stay in one file (chapter 6) |
Variants with cva | the repeated thing has options | a dependency; unknown values fail silently without types |
@apply in @layer components | plain HTML or a CMS with no component system | a second place to look; utilities still override it because of layer order |
| Leave it long | the list is used once | nothing; a long list used once is fine |
Catching Visual Changes
A class change can break a page without any test failing. The cheapest check is a screenshot comparison. I rendered a small card page in headless Chrome (--screenshot), took screenshots of the same page,
and compared them with pixelmatch:
On one machine with the same browser, two runs of an unchanged page were identical, so any difference was a real change. A one-shade colour change was caught at both thresholds. Two cautions that I did not test: screenshots from a different operating system or browser version will differ in text rendering, so compare only against screenshots from the same environment (usually your CI image), and a threshold that is too loose will hide small changes like the shade above. Tools such as Playwright's screenshot comparison automate this; I did not run it.
A Review Checklist for Tailwind Markup
- Complete class names. No
'bg-' + color(chapter 1). Variant maps contain whole classes. - Conflicts. No two classes setting the same property on one element; component
classNamegoes throughcn(chapter 6), and ahover:counterpart is overridden too. - Tokens, not literals. A colour or size used in more than one place is a theme token (chapter 2), not
bg-[#3b82f6]repeated. - State uses the right attribute.
data-*oraria-*variants match the library's real attributes (chapter 4 and 6); grep the built CSS if unsure. - Sorted and linted. Prettier and ESLint pass with no per-file exceptions.
- Length. A class list repeated three times becomes a component or variant.
- Browser targets. No bleeding-edge utility the targets lack.
- Visual check. The screenshot diff for changed components was looked at, not just passed.
2. I timed
npx, not the build. Every run took about 3.5 seconds; the build itself took 120 to 330 ms. How to avoid it: time the program directly and compare with its own timer.3.
--watch exited immediately without watching. In a non-interactive shell it needs --watch=always. How to avoid it: look for the first "Done" line in the log before editing any file.4. A test that could not show a difference. I limited the scan to a glob that, as it happened, matched 111 of 500 files, which together contained every one of the 150 classes, so the size was unchanged. I dropped it. How to avoid it: work out in advance what result would prove the claim, and build the input so that result is possible.
5. I copied the plugin's README config and it failed (
configs["flat/recommended"] is not iterable). How to avoid it: print Object.keys(plugin.configs) for the version you installed.
Hands-On Exercises
Generate three 500-file projects with different numbers of distinct classes (and one with variant prefixes), build each from inside its own folder, and report plain, minified and gzipped sizes. Then time the same build with npx and by calling the CLI directly, and measure one --watch=always rebuild.
Configure Prettier with the Tailwind plugin and ESLint with one of the two Tailwind plugins. Run them on a file with a conflict, a typo and an unsorted list, show the report, fix it, and show that Prettier and ESLint then agree. Include a custom theme colour and a cva call.
Screenshot a small page with headless Chrome, change one class, screenshot again and count differing pixels with pixelmatch. Include the unchanged-twice control and compare two thresholds on a subtle colour change.
Chapter 7 Quick Reference
- Output size follows distinct class-plus-variant combinations, not files or repeats: 150 classes 13.6 KB, 785 classes 41.7 KB, 4,513 classes (with variants) 239 KB minified; gzipped 3.1, 7.1 and 29.0 KB
theme(static)doubled the 150-class output; dropping preflight saved 3.6 KB but removes the reset;--minifyshrinks,--optimizedoes not- Time the program, not
npx. Cold build of 500 files: 120 to 330 ms; watch edits mostly under 100 ms; CSS edits about a cold build. Use--watch=alwaysin scripts - v4 needs Chrome 111, Safari 16.4, Firefox 128 (Tailwind's compatibility page); the output uses layers,
@property,color-mix(),oklch() - Prettier:
plugins,tailwindStylesheet(needed to know theme names),tailwindFunctions(needed forcva); strings insideclassNameare sorted anyway - ESLint:
eslint-plugin-tailwindcss(cssConfigPath) oreslint-plugin-better-tailwindcss(entryPoint); both caught conflicts and the typo. Turn off the second one's line-wrapping rule if you use Prettier - Extract repeated markup into components or variants for maintenance; the transfer saving was 12% gzipped
- Screenshots from the same browser and machine were byte-identical, so a pixel diff is meaningful; keep references per environment