Performance & Production Builds
A Tailwind site can have tiny CSS: often a few kilobytes for a small site and still modest for a large app, because the build only includes classes you actually use. But that only works if the build can find your classes, and if you ship the result the right way. Many "it worked on my laptop but not on the server" problems come from this step.
In this lesson you will learn exactly which files Tailwind scans, how to add or exclude folders, how to force classes into the build, how to make and serve a production build, and how to debug a class that is missing on the live site.
How small is the CSS?
Every page's CSS contains the variables it uses, the Preflight reset and the utilities it uses, nothing more. Run this page: a small script measures the CSS that the browser build generated for it.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>CSS size</title>
<script src="https://cdn.jsdelivr.net/npm/@tailwindcss/browser@4.3.3"></script>
</head>
<body class="bg-slate-100 p-8">
<article class="max-w-sm rounded-xl bg-white p-6 shadow-md">
<h1 class="text-xl font-bold text-slate-900">Rohtak Book Fair</h1>
<p class="mt-2 text-slate-600">10 to 16 November, open to all.</p>
<p id="size" class="mt-4 rounded-lg bg-sky-50 p-3 text-sm text-sky-900">Measuring...</p>
</article>
<script>
setTimeout(() => {
const css = [...document.querySelectorAll('style')].map(s => s.textContent).join('');
const kb = (new Blob([css]).size / 1024).toFixed(1);
document.getElementById('size').textContent = `Tailwind generated ${kb} KB of CSS for this page (before minifying and compressing).`;
}, 500);
</script>
</body>
</html>The browser build puts the CSS it generates in a <style> element, which the script reads. A real build produces the same kind of output as a file, and minifying and compressing it make it several times smaller again. Add a few classes to the card and run it again; the size grows only a little.
What the build scans
Tailwind 4 finds your template files automatically. It starts from the current working directory (usually your project folder) and scans every file except:
- files and folders listed in your
.gitignore; - the
node_modulesfolder; - binary files such as images, videos and zip files;
- CSS files;
- package manager lock files.
That covers HTML, JavaScript, JSX, PHP, Blade, Vue and every other template format without any configuration. Tailwind reads them as plain text, which is why class names must appear in full (see How Utility Classes Become CSS).
Adding and excluding sources
Sometimes classes live in a place the scanner skips, or you want it to skip a folder:
@import "tailwindcss";
/* Scan a UI library installed in node_modules */
@source "../node_modules/@acme/ui-kit";
/* Scan Laravel's compiled views, even though storage/ is in .gitignore */
@source "../storage/framework/views";
/* Do not scan old templates that are no longer used */
@source not "../legacy";Paths in @source are relative to the CSS file that contains them. To choose the folder automatic detection starts from, add source() to the import: @import "tailwindcss" source("../src");. To turn automatic detection off completely and list every folder yourself, use source(none).
Safelisting: forcing classes into the build
If class names come from somewhere Tailwind cannot scan, such as a database or an API, you can force them into the build with @source inline(). It understands brace expansion, so one line can produce many classes:
@import "tailwindcss";
/* Exactly these classes */
@source inline("bg-green-100 text-green-800 bg-red-100 text-red-800");
/* bg-amber-50, 100, 200 ... 900, 950, each also with hover: */
@source inline("{hover:,}bg-amber-{50,{100..900..100},950}");Use this sparingly: every safelisted class is shipped even if no page uses it. A better design is usually to store a status in the database ("paid", "due") and map it to full class names in your template, as shown in Reusable Components. There is also @source not inline(...) to block classes that the scanner picks up by mistake.
Making the production build
- Vite:
npm run buildwrites optimised, minified CSS and JavaScript todist, with a content hash in each file name such asindex-4f2a9c.css. - CLI: run once with
--minify:npx @tailwindcss/cli -i ./src/input.css -o ./assets/app.css --minify. - PostCSS: your framework's normal build command runs Tailwind and minifies.
Tailwind 4 also handles browser prefixes and modern CSS such as nesting internally (it uses a tool called Lightning CSS), so you no longer need autoprefixer. The CSS it outputs targets modern browsers: Safari 16.4+, Chrome 111+ and Firefox 128+. If you must support older browsers, Tailwind v3.4 is the documented fallback.
Serving it fast
A small CSS file is only half the story. On the server:
- Compress text files with gzip or Brotli. Most hosts, including typical shared hosting, can switch this on. CSS full of repeated class patterns compresses very well.
- Cache for a long time when file names contain a hash. When the CSS changes, the name changes, so visitors never get a stale file. Without hashes, add a version to the URL:
app.css?v=2026-11-02. - Load the CSS early with a
<link rel="stylesheet">in the<head>, never from JavaScript, so the page is styled on the first paint. - Never ship the browser build on a live site; it downloads a compiler and builds CSS on every visit.
Debugging a class that is missing in production
If a class works locally but not on the live site, check these in order:
- Was the CSS rebuilt? If you used the watcher locally, run the production build again and upload the new file.
- Is the class name complete in the source?
`text-${colour}-600`never works in a real build. - Is the file scanned? Files in
.gitignored folders, or outside the base path, are skipped. Add an@source. - Is the browser loading the old file? Hard-refresh, or check the file name and hash in DevTools.
Tailwind's CLI also has a --watch mode for development and an --optimize flag that optimises without minifying, which keeps the output readable when you need to inspect it.
Common mistakes
- Uploading the development CSS. Always run the production build before deploying.
- Safelisting whole palettes. Thousands of unused classes undo the size advantage. Map data to known class names instead.
- Generated templates in ignored folders. Framework caches and build folders are often gitignored; add an
@sourceif your classes live there. - Keeping
autoprefixerandpostcss-import. They are not needed with Tailwind 4 and can only slow the build.
Key points
- Tailwind outputs only the classes it finds, so production CSS is small.
- Automatic detection scans the project but skips gitignored files,
node_modules, binaries, CSS and lock files. @source "path"adds folders,@source not "path"excludes them, andsource()on the import sets the base path.@source inline("...")safelists classes, with brace expansion; use it sparingly.- Build with
npm run build(Vite) or--minify(CLI), then serve with compression and long caching for hashed files. - Tailwind 4 targets Safari 16.4+, Chrome 111+ and Firefox 128+, and handles prefixes itself.
Exercise
A school website in Kochi stores notice categories ("exam", "holiday", "event") in MySQL, and the PHP template prints them straight into a class: class="bg-<?= $cat ?>-colour". On the live site the colours are missing. Explain the problem, then write (1) a PHP-friendly fix using a map of full class names, and (2) the @source inline() line you would use instead if you could not change the template, for bg-red-100, bg-green-100 and bg-sky-100.
Show solution
The class names only exist after PHP runs, so the Tailwind build never sees them and generates no CSS. The best fix is to map each category to complete class names in the template:
<?php
$categoryClasses = [
'exam' => 'bg-red-100 text-red-800',
'holiday' => 'bg-green-100 text-green-800',
'event' => 'bg-sky-100 text-sky-800',
];
$classes = $categoryClasses[$notice['category']] ?? 'bg-slate-100 text-slate-800';
?>
<p class="rounded-lg p-3 <?= $classes ?>"><?= htmlspecialchars($notice['title']) ?></p>If the template cannot change, safelist the classes in the main CSS file:
@source inline("bg-{red,green,sky}-100");The map is better because the template only uses known values, and unknown categories fall back to a safe grey style instead of breaking.
Quick Quiz
1. Which files does Tailwind's automatic source detection skip?
Automatic detection scans everything else in the project, including PHP, JS and HTML templates.
2. Your UI library in node_modules/@acme/ui-kit uses Tailwind classes, but they are missing. What do you add?
node_modules is skipped by default, so register the library's folder with @source.
3. What does @source inline("{hover:,}underline") generate?
Brace expansion produces one class with the hover: prefix and one with an empty prefix.
4. Which CLI flag makes a smaller production CSS file?
--minify (or -m) optimises and minifies the output for production.
Learn Tailwind CSS with a mentor
Free lessons get you started. Our Full Stack Web Development training adds real projects and code reviews from working developers.
- Project-based learning
- Mentor code reviews
- Free counselling call
