from long CI runs to second scale builds
Anyone running Tailwind CSS in a monorepo with many packages knows the problem: the build process goes from seconds to minutes once the content configuration scans too many files. With targeted globs, Turborepo caching, Nx task pipelines, and parallelization, build time can be kept consistently low even across hundreds of packages.
Table of Contents
- 1. Why build times grow in monorepos
- 2. Narrowing content globs deliberately
- 3. Turborepo: remote caching for Tailwind builds
- 4. Nx: task pipelines and affected builds
- 5. Incremental compilation in watch mode
- 6. Parallelizing multiple packages
- 7. Reducing PostCSS pipeline overhead
- 8. CI/CD specific caching strategies
- 9. Measure, do not guess: build times compared
- 10. Summary
- 11. FAQ
1. Why build times grow in monorepos
Build time optimization usually becomes a topic in Tailwind projects only once a monorepo grows from two to twenty packages and the once fast build suddenly starts costing noticeable time. The cause rarely lies in Tailwind itself, but in the content configuration: every additional glob path means the scanner has to read and search more files for class names. A monorepo with shared UI packages, several apps, and shared component libraries adds these costs up quickly to several seconds per build, which becomes a real bottleneck with frequent CI runs.
A second factor behind long build times is the missing separation between a local development build and a CI build. In watch mode, Tailwind CSS v4 keeps the state of the last scan in memory and benefits from a warm file system cache, while a fresh CI runner starts from zero every time: installing node_modules, scanning the entire content tree, regenerating the full CSS. Without deliberate build time optimization, every pull request pipeline pays this full price, even if only a single component has changed. The following sections show concrete levers that structurally reduce these costs in monorepos.
2. Narrowing content globs deliberately
The most effective step for build time optimization is almost always the content configuration itself. A common mistake in monorepos: the root Tailwind config points to the entire workspace with a single broad glob like ../../packages/**/*.{js,ts,jsx,tsx}, including build artifacts, test files, and node_modules leftovers that were accidentally not excluded. Every one of these files gets read by the scanner even though it never contains a Tailwind class. The fix is to define a narrow, package specific glob pattern per package and only include directories that actually matter for UI.
It is also worth checking file extensions: anyone keeping test files, Storybook stories, or generated .d.ts files in the same directory as components should explicitly exclude them from the glob instead of scanning them implicitly. This fine tuning of the content configuration is not a one time task but should be reviewed with every new package added to the monorepo, because imprecise globs quickly add up to noticeable seconds per build in large codebases.
/* apps/storefront/tailwind.config.css — narrow, package-specific content globs */
@import "tailwindcss";
/* Only scan this app's own source, not the entire monorepo */
@source "./src/**/*.{ts,tsx}";
/* Shared UI package: explicit, not a broad workspace-wide glob */
@source "../../packages/ui/src/**/*.{ts,tsx}";
/* Exclude generated and test files even inside included folders */
@source not "./src/**/*.stories.tsx";
@source not "./src/**/*.test.tsx";
@source not "../../packages/ui/dist/**/*";
3. Turborepo: remote caching for Tailwind builds
For build time optimization, Turborepo brings a feature that often goes unused in classic Tailwind setups: task hashing with remote caching. Every build task gets a hash assigned from its input files (content sources, config, dependencies). If nothing about these inputs changes, Turborepo serves the cached CSS result directly from the cache without even starting the Tailwind compiler. In a monorepo with twenty packages, where a typical pull request only touches two of them, this saves the entire build time for the remaining eighteen packages.
Correct working caching depends on a proper outputs declaration in turbo.json, so Turborepo knows which generated CSS files belong to the cache entry. If the CSS output is not declared correctly, Turborepo still caches the task status but returns no usable output, which in practice leads to confusing "cache hit but CSS missing" situations. Remote caching via Vercel or a self hosted cache server extends this benefit beyond individual machines: a CI runner can benefit from a build that ran minutes earlier on a completely different runner.
{
"$schema": "https://turbo.build/schema.json",
"remoteCache": { "enabled": true },
"tasks": {
"build:css": {
"dependsOn": ["^build:css"],
"inputs": [
"src/**/*.{ts,tsx}",
"tailwind.config.css",
"../../packages/ui/src/**/*.{ts,tsx}"
],
"outputs": ["dist/**/*.css"],
"cache": true
}
}
}
4. Nx: task pipelines and affected builds
Nx takes a similar approach to build time optimization as Turborepo but adds the project graph and the affected command. Instead of building every package every time, Nx determines from Git diffs which projects are actually affected by a change and rebuilds only their Tailwind CSS. An isolated fix in a utility package that no other package imports no longer triggers a full monorepo build.
Nx Cloud extends local caching with distributed task execution: several CI agents can run independent Tailwind builds in parallel and store their results in the same cache, so subsequent pipelines benefit directly. For build time optimization in large organizations with many teams working in parallel, this is often the bigger lever than purely local caching strategies, because the time saved accumulates across every developer and pipeline.
{
"targetDefaults": {
"build:css": {
"cache": true,
"inputs": ["{projectRoot}/src/**/*.tsx", "{projectRoot}/tailwind.config.css"],
"outputs": ["{projectRoot}/dist/**/*.css"],
"dependsOn": ["^build:css"]
}
}
}
// CLI: only rebuild Tailwind CSS for projects affected by the current diff
// npx nx affected --target=build:css --base=origin/main
5. Incremental compilation in watch mode
An often overlooked aspect of build time optimization is the difference between a full build and incremental watch mode. In local development, Tailwind CSS v4 keeps the state of the last scan in memory and only needs to rescan changed sources on a file change, instead of reading the entire content tree again. This incrementality is the main reason local rebuilds usually finish in milliseconds, while a CI full build takes several seconds.
In practice this means for monorepo setups: watch mode performance and CI build performance are two separate optimization problems that need different solutions. Watch mode benefits from smaller, granular packages and fast file system access, while CI builds benefit from caching layers like Turborepo or Nx that avoid the full scan process altogether. Anyone who only optimizes watch mode but leaves the CI pipeline unchanged will still experience long wait times on every pull request.
6. Parallelizing multiple packages
Besides caching, parallelization is the second major lever for build time optimization in monorepos. Since every package typically has its own independent Tailwind build, independent builds can easily run on multiple CPU cores at the same time instead of being processed sequentially. Both Turborepo and Nx parallelize by default as long as dependencies between packages are correctly declared in the project graph.
A common mistake here: a shared UI package is not declared as a dependency, so dependent apps start their build before the UI package has finished compiling. This not only leads to inconsistent CSS but forces the build system, in the worst case, to repeat the entire process serially with retries. A clean dependsOn configuration in turbo.json or project.json ensures parallelization only applies where it is actually safe, reliably preventing such race conditions.
# Run independent Tailwind builds in parallel across CPU cores
# Turborepo automatically parallelizes tasks without declared dependencies
npx turbo run build:css --concurrency=8
# Nx equivalent: parallel flag controls max concurrent tasks
npx nx run-many --target=build:css --parallel=8 --all
7. Reducing PostCSS pipeline overhead
Even outside Tailwind itself, the PostCSS pipeline contributes to overall build time, and this is an often overlooked point in build time optimization. Plugins like Autoprefixer or cssnano add extra processing steps to every single build. In development builds, minification is usually unnecessary because the result only ever lands in a local browser anyway. A conditional PostCSS configuration that only activates cssnano in the production build saves measurable milliseconds in every watch mode cycle.
Another point is plugin order: Autoprefixer should always run after Tailwind itself, never before, otherwise generated utility classes no longer receive correct vendor prefixes and the entire step becomes ineffective without any visible error. For monorepos with many packages, it is also worth maintaining a shared, central PostCSS config referenced by all packages, instead of maintaining a separate copy in each one that easily drifts apart over updates.
8. CI/CD specific caching strategies
Beyond Turborepo and Nx caching, there are general CI/CD techniques that further improve build time optimization. Caching node_modules between pipeline runs based on the lockfile hash saves the installation time that would otherwise be paid on every run. Docker layer caching for containerized build environments ensures unchanged dependency layers do not get rebuilt as long as package.json and the lockfile have not changed.
It is important to also keep the Turborepo or Nx cache folder persistent across CI runs, for example via the GitHub Actions cache action or a dedicated object storage bucket. Without this persistent cache, local build time optimization completely evaporates at the CI level, because every runner starts from zero. A combination of dependency caching, persistent task cache, and remote caching server delivers the largest cumulative time savings in practice across many pipeline runs.
# .github/workflows/build.yml — layered caching for Tailwind monorepo builds
name: build
on: [pull_request]
jobs:
css-build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with: { fetch-depth: 0 } # needed for nx affected / turbo diff detection
- uses: actions/setup-node@v4
with: { node-version: 20 }
- name: Cache node_modules
uses: actions/cache@v4
with:
path: node_modules
key: node-modules-${{ hashFiles('package-lock.json') }}
- name: Cache Turborepo task outputs
uses: actions/cache@v4
with:
path: .turbo
key: turbo-${{ github.sha }}
restore-keys: turbo-
- run: npm ci
- run: npx turbo run build:css --concurrency=8
9. Measure, do not guess: build times compared
Every build time optimization should be backed by measurements, not assumptions. Node comes with time and Turborepo with --summarize as built in tools to precisely capture build durations and catch regressions early. A simple benchmark script that measures several consecutive runs and takes the median delivers more reliable numbers than a single stopwatch measurement that can be skewed by operating system noise.
| Setup | Cold build (20 packages) | Cached build (2 changed) | Note |
|---|---|---|---|
| No caching | ~48 s | ~48 s | Every build is a full build |
| Turborepo local | ~46 s | ~5 s | Only rebuild affected packages |
| Turborepo + remote cache | ~9 s | ~3 s | Cache hit across other runners |
| Nx + Nx Cloud | ~8 s | ~3 s | Distributed task execution possible |
These example values come from a realistic monorepo setup with twenty packages and show how strongly caching strategies affect build time optimization once only a fraction of packages have actually changed. The biggest jump comes from remote caching, because it benefits not just the local machine but the entire organization from every single build.
Mironsoft
Frontend performance, build pipelines, and monorepo architecture
Tailwind builds that stay fast even in large monorepos?
We analyze existing content configurations, set up Turborepo or Nx caching, and optimize CI pipelines so your Tailwind builds finish in seconds instead of minutes, even as the package count grows.
Build audit
Analysis of content globs and identification of unnecessary scan costs
Caching setup
Turborepo remote caching or Nx Cloud set up to fit your team size
CI pipeline
Dependency caching, Docker layer caching, and persistent task caches
10. Summary
Build time optimization for Tailwind CSS in monorepos starts with the content configuration and ends with the CI pipeline. Narrow content globs prevent the scanner from reading unnecessary files. Turborepo and Nx bring task hashing and caching that completely skip repeated builds of identical inputs. Affected builds ensure only actually changed packages get recompiled instead of building the entire monorepo every time.
Parallelization across multiple CPU cores, a conditional PostCSS pipeline without unnecessary minification in dev mode, and persistent CI caching for node_modules and task results complete this foundation. Anyone who measures build times regularly instead of just guessing will catch regressions early and can steer the process precisely before a once fast build silently turns into a bottleneck for the entire team.
Build Time Optimization for Tailwind CSS in Monorepos — Key Takeaways
Content globs
Use narrow, explicit glob patterns per package instead of one broad workspace wide pattern.
Caching
Use Turborepo or Nx with remote caching so unchanged builds come straight from the cache.
Affected builds
Only recompile actually affected packages based on the project graph and Git diffs.
Measure
Capture build times regularly with benchmarks to catch regressions early.