Build Time Optimization for Tailwind CSS in Monorepos
AI generated
</>
tw
Tailwind CSS · Build Time · Monorepo · CI/CD
Build Time Optimization for Tailwind CSS in Monorepos
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.

18 min read Turborepo · Nx · Content Globs · CI Caching Tailwind CSS v4 · Node 20+

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.

11. FAQ: Build Time Optimization for Tailwind CSS

1Why does the build get slower over time?
Content globs grow with every new package, the scanner reads more and more files. Without caching, every build becomes a full scan.
2Fastest first step?
Narrow the content configuration per package and explicitly exclude test and generated files.
3Turborepo or Nx?
Both bring caching and parallelization. Nx stands out with affected builds, Turborepo with simpler configuration.
4What is remote caching?
Centrally stored build results that other machines or CI runners can reuse.
5Why doesn't watch mode help CI?
Watch mode uses a warm process cache. CI runners start cold and need their own caching layers.
6How do I measure build times correctly?
Measure several runs and take the median. Turborepo offers its own reports via --summarize.
7Can parallelization hurt?
Too much concurrency on few cores creates context switching overhead. Match concurrency to available cores.
8Does caching node_modules help?
Yes, it saves installation time paid regardless of the Tailwind build. Combined with task caching, the biggest lever.
9Autoprefixer and cssnano on every build?
No, activate cssnano only in the production build. Saves unnecessary processing steps in the development cycle.
10Incorrect outputs in turbo.json?
Turborepo reports a cache hit but returns no usable CSS file. Check the outputs path carefully.