production optimization

[check: …]
📖 Pair this live Tailwind demo with the companion guide (.md) — this page is the rendered ground truth. ↗ Pairs with tailwind/build_tooling (the CLI / Vite / PostCSS pipeline this page measures) and tailwind/source_detection (the @source directive that decides what gets tree-shaken).

the optimization pipeline — source → detect → compile → minify → ship

A Tailwind v4 production build is a 5-stage funnel: every stage removes bytes. Content detection throws away the ~10,000 utilities you didn't use; Lightning CSS minifies what remains (shorter hex, merged rules, dropped duplicate selectors); Browserslist decides which modern CSS features get polyfilled down. The result fits in a single small request — typically 8-15KB gzipped.

stage 1
source files
your .html, .tsx, .vue — the raw class strings
stage 2
detect
plain-text crawl (no AST). @source controls scope.
stage 3
compile
only tokens that map to a utility emit CSS — tree-shaking.
stage 4
minify
Lightning CSS: shorter hex, merged rules, dropped dupes.
stage 5
ship
one dist.css (gzipped). 8-15KB typical.

The Play CDN (this very page) does stages 2-3 in the browser at runtime via a MutationObserver — that's why it's ~300KB+ and is never for production. The CLI does the same work ahead of time and ships only stage 5.

1 · toggle optimizations → see estimated output size

Model of a typical marketing site (~350 unique utilities used). Toggle each stage and watch the estimated shipped size. All on = real production build. All off ≈ what you'd ship if you naively inlined the entire framework.

content detection (tree-shake)
minify (Lightning CSS)
Lightning CSS advanced (merge + shorten)
gzip transport
0 KB estimated shipped CSS · 350 utilities
this build
Play CDN (engine) ~300 KB
budget (20KB gz) 20 KB

utilities emitted: — · stages active: —

2 · CSS size comparison — Play CDN vs production build

These are observed ranges for the same ~350-utility marketing site. The Play CDN ships the entire JIT engine + every utility it might need; the production build ships only what your source references.

approachrawminifiedgzippedfor production?
Play CDN <script> ~1.2 MB ~650 KB ~300 KB+ ❌ never — it's the JIT engine, not your CSS
v4 CLI build --minify ~30 KB ~12 KB ~4-8 KB ✅ yes — only used utilities
v4 CLI build (no minify) ~30 KB ~10 KB ⚠ only for debugging
full Tailwind (all utilities, minified) ~2 MB ~600 KB ~200 KB ❌ you accidentally disabled detection
Preflight reset (always included) ~8 KB ~6 KB ~3 KB — baseline floor you can't drop below

The takeaway: the gzipped production build is roughly 40-75× smaller than the Play CDN. That gap is the entire reason a build step exists.

3 · browser targeting — Browserslist decides what gets polyfilled

v4 uses Lightning CSS, which reads your browserslist field (in package.json) and down-levels modern CSS only when a targeted browser lacks native support. Target newer browsers → smaller output (fewer polyfills). Target older ones → Lightning CSS expands oklch(), nesting, color-mix(), and @layer.

/* package.json */ { "browserslist": [ "defaults", "fully supports es6-module" ] }
modern CSS feature Chrome 120+ Safari 17+ Safari 15.4 Firefox 115 (ESR)
oklch() colors nativenative → rgb/hsl polyfill native
CSS nesting (&) native → expanded → expanded → expanded
:has() selector nativenativenative not supported (FF 121+)
@layer cascade nativenativenativenative
color-mix() nativenative → flattened native
relative color syntax native → flattened → flattened → flattened

Gotcha: v4 dropped IE entirely — Lightning CSS will not polyfill for it. If your browserslist includes IE, the build will emit a warning and skip those targets.

4 · pinned builds — byte-reproducible output

Pin the exact CLI version to make builds reproducible across CI and developers. @latest can shift your output when a minor release changes minification heuristics.

# Pin exact version for byte-reproducible builds npx @tailwindcss/cli@4.3.1 -i app.css -o dist.css --minify # Watch mode (dev) — rebuilds on source change npx @tailwindcss/cli@4.3.1 -i app.css -o dist.css --watch # Or install locally and reference the binary npm i -D @tailwindcss/cli@4.3.1 ./node_modules/.bin/tailwindcss -i app.css -o dist.css --minify

5 · splitting strategies — beyond one big file

strategyhowtradeoff
route-based splitting one CSS file per route (e.g. home.css, dashboard.css) — bundler splits via dynamic import() smaller initial download; extra request on navigation; shared utilities dedupe across files
critical CSS extraction inline only above-the-fold CSS in <style>, defer the rest fastest first paint; tooling complexity (critters, penthouse)
lazy-load non-critical <link rel="preload" as="style" onload="..."> for below-the-fold defers render-blocking; needs JS fallback for no-JS clients
single bundle (default) one dist.css for the whole app simplest; best gzip; fine when total < 20KB gzipped

Rule of thumb: if your single gzipped bundle is under ~20KB, don't split — the HTTP overhead outweighs the savings. Splitting only pays off above ~30KB or when routes have very different utility sets.

intent → pattern

intentpatternwhy
ship the smallest CSS npx @tailwindcss/cli@4.3.1 -i app.css -o dist.css --minify tree-shake + Lightning CSS minify in one step
only scan specific files @source "../templates"; in your CSS restricts content detection → tighter output. See source_detection.
turn auto-detection OFF @import "tailwindcss" source(none); only @source-registered paths are scanned
control polyfill targets "browserslist": ["defaults"] in package.json newer targets → fewer polyfills → smaller CSS
byte-reproducible CI pin @tailwindcss/cli@4.3.1 (never @latest) minification heuristics can change between minors
force-include a dynamic class @source inline("hidden"); v4 safelist — for classes constructed at runtime
inspect what shipped ls -la dist.css && gzip -c dist.css | wc -c verify the real gzipped byte count, not estimates