| |

Tailwind CSS 74 🎨 CI/CD Unused Class Auditing and CSS Bundle Size Budgeting

⏱️ Estimated read: 13 min | |

A CSS bundle grows. The feature ships, the class is added, the feature is removed, and the class remains. The @apply rules accumulate, the custom utilities persist, and the design system evolves. The bundle that was fifty kilobytes at the launch is two hundred kilobytes a year later, and no one noticed because the growth was gradual and the build never failed. The CI/CD auditing and the size budgeting are the mechanisms that make the growth visible, the regression caught, and the size the enforced. This chapter covers the tools, the budgets, and the pipeline.

Key point: Tailwind’s JIT engine already removes the unused utilities. The class that is not in the source is not in the output. The unused-class auditing is about the classes the engine cannot see: the dynamic constructions, the @apply rules, the custom utilities that are no longer referenced, and the safelist entries that outlived their purpose.


Why the auditing and the budgeting matter

The gradual-growth problem. The bundle grows a few kilobytes per feature. The individual change is invisible, and the cumulative growth is the problem. The budget is the threshold, and the CI fails when the budget is exceeded.

The unused-class problem. The Tailwind JIT generates the CSS for the candidates it finds. The candidate that is no longer in the source is no longer in the output. The @apply rule that references the removed utility remains until the rule is removed.

The @apply problem. The @apply inlines the utility into the custom CSS. The rule is not the candidate, and the engine does not track it. The @apply that references the removed utility produces the error, and the @apply that is no longer used produces the dead CSS.

The custom-utility problem. The @utility registers the custom utility. The utility that is no longer referenced in the markup is the dead CSS, and the engine generates it because the @utility is the registration.

The safelist problem. The @source inline(...) and the legacy safelist preserve the class. The entry that was the workaround for the dynamic class remains after the dynamic class is refactored. The dead CSS.

The dependency problem. The design system package ships the classes. The class that is the consumer’s usage is the bundle. The audit is the cross-repository.

The budget problem. The budget is the number in the CI. The pull request that pushes the bundle over the budget fails. The author must justify the increase or the optimize the change.


a. The unused-class auditing

The audit finds the classes the engine generated but the application no longer uses.

The Tailwind’s built-in reporting

The v4 engine does not report the unused classes by default. The class that is not in the source is not generated. The audit is the reverse: the class that is generated but not used.

The class-usage report

The plugin that reports the class usage. The tailwindcss-classnames and the similar tools produce the report.

npx tailwindcss-classnames --input src --output report.json

The report lists the classes and the usage counts.

The @apply audit

The @apply rules are the source of the dead CSS. The audit lists the rules and the utilities they reference.

# Find the @apply rules
grep -r "@apply" src/

# The output
src/components/Button.css:.btn { @apply bg-blue-600 px-4; }
src/components/Card.css:.card { @apply rounded-lg shadow-md; }

The rule that is no longer used is the candidate for the removal.

The custom-utility audit

The @utility registrations are the custom utilities. The audit lists the utilities and the markup usage.

# Find the @utility registrations
grep -r "@utility" src/

# Find the usage in the markup
grep -r "tab-4" src/

The utility that is not in the markup is the dead CSS.

The safelist audit

The @source inline(...) and the safelist are the preserved classes. The audit lists the entries and the reason.

# Find the safelist entries
grep -r "@source inline" src/

The entry that is no longer needed is the removal candidate.

The audit table

The auditThe tool
The class usageThe tailwindcss-classnames
The @applyThe grep
The @utilityThe grep
The safelistThe grep
The dependencyThe cross-repo

b. The CSS bundle size budgeting

The budget is the threshold the CI enforces. The bundle that exceeds the budget fails the build.

The bundle size measurement

The build produces the CSS file, and the size is the measurement.

# The build
npm run build

# The size
ls -lh dist/assets/*.css
# -rw-r--r-- 1 user user 48K Jan 15 10:30 dist/assets/index.a1b2c3d4.css

The size is the raw, and the compression is the separate.

The compression measurement

The gzip and the brotli sizes are the transfer size.

# The gzip
gzip -c dist/assets/index.a1b2c3d4.css | wc -c
# 12288

# The brotli
brotli -c dist/assets/index.a1b2c3d4.css | wc -c
# 10240

The transfer size is the number that matters for the user.

The size-limit

The size-limit is the popular tool. It measures the build and the fails when the budget is exceeded.

// package.json
{
  "size-limit": [
    {
      "path": "dist/assets/*.css",
      "limit": "50 KB",
      "gzip": true
    },
    {
      "path": "dist/assets/*.js",
      "limit": "200 KB",
      "gzip": true
    }
  ]
}

The path is the glob, the limit is the threshold, and the gzip is the compression.

The size-limit command

npx size-limit

The command measures and the reports. The exit code is the non-zero when the budget is exceeded.

The bundlewatch

The bundlewatch is the similar tool with the GitHub integration.

// bundlewatch.config.json
{
  "files": [
    {
      "path": "./dist/assets/*.css",
      "maxSize": "50 kB"
    }
  ]
}

The bundlewatch compares with the base branch and the comments on the pull request.

The CI integration

# .github/workflows/size.yml
name: Size

on: [pull_request]

jobs:
  size:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: 20
      - run: npm ci
      - run: npm run build
      - run: npx size-limit

The workflow runs on the pull request, and the failure blocks the merge.

The budget table

The toolThe purpose
size-limitThe local budget
bundlewatchThe GitHub integration
The gzipThe transfer size
The brotliThe transfer size
The CI workflowThe enforcement

c. The CI/CD pipeline

The pipeline combines the audit, the build, and the budget check.

The full pipeline

# .github/workflows/ci.yml
name: CI

on: [push, pull_request]

jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: 20
          cache: npm
      - run: npm ci
      - run: npm run lint
      - run: npm run test
      - run: npm run build
      - run: npx size-limit

The lint, the test, the build, and the size-limit run in the order.

The audit step

      - name: Audit the unused classes
        run: |
          npx tailwindcss-classnames --input src --output report.json
          npx ts-node scripts/audit-unused.ts

The audit produces the report, and the script checks the thresholds.

The custom audit script

// scripts/audit-unused.ts
import { readFileSync } from 'fs';

const report = JSON.parse(readFileSync('report.json', 'utf-8'));
const unused = report.filter((c) => c.usage === 0);

if (unused.length > 0) {
  console.error('Unused classes:', unused);
  process.exit(1);
}

The script reads the report and the fails when the unused exist.

The budget check step

      - name: Check the CSS budget
        run: |
          CSS_SIZE=$(gzip -c dist/assets/*.css | wc -c)
          BUDGET=51200
          if [ "$CSS_SIZE" -gt "$BUDGET" ]; then
            echo "CSS exceeds the budget: $CSS_SIZE > $BUDGET"
            exit 1
          fi

The step measures the gzip size and the compares with the budget.

The pull request comment

      - name: Comment the size
        uses: actions/github-script@v7
        with:
          script: |
            const size = require('fs').statSync('dist/assets/index.css').size;
            github.rest.issues.createComment({
              issue_number: context.issue.number,
              owner: context.repo.owner,
              repo: context.repo.repo,
              body: `CSS size: ${size} bytes`,
            });

The step comments the size on the pull request.

The pipeline table

The stepThe purpose
The lintThe code style
The testThe correctness
The buildThe output
The auditThe unused classes
The size-limitThe budget
The commentThe visibility

Complete Example Session

// ============================================
// PART 1: THE SIZE-LIMIT CONFIG
// ============================================

// package.json
{
  "size-limit": [
    {
      "path": "dist/assets/*.css",
      "limit": "50 KB",
      "gzip": true
    }
  ]
}

// ============================================
// PART 2: THE BUNDLEWATCH CONFIG
// ============================================

// bundlewatch.config.json
{
  "files": [
    {
      "path": "./dist/assets/*.css",
      "maxSize": "50 kB"
    }
  ]
}

// ============================================
// PART 3: THE FULL CI WORKFLOW
// ============================================

// .github/workflows/ci.yml
name: CI

on: [push, pull_request]

jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: 20
          cache: npm
      - run: npm ci
      - run: npm run lint
      - run: npm run test
      - run: npm run build
      - run: npx size-limit

// ============================================
// PART 4: THE AUDIT STEP
// ============================================

- name: Audit the unused classes
  run: |
    npx tailwindcss-classnames --input src --output report.json
    npx ts-node scripts/audit-unused.ts

// ============================================
// PART 5: THE AUDIT SCRIPT
// ============================================

// scripts/audit-unused.ts
import { readFileSync } from 'fs';

const report = JSON.parse(readFileSync('report.json', 'utf-8'));
const unused = report.filter((c) => c.usage === 0);

if (unused.length > 0) {
  console.error('Unused classes:', unused);
  process.exit(1);
}

// ============================================
// PART 6: THE BUDGET CHECK
// ============================================

- name: Check the CSS budget
  run: |
    CSS_SIZE=$(gzip -c dist/assets/*.css | wc -c)
    BUDGET=51200
    if [ "$CSS_SIZE" -gt "$BUDGET" ]; then
      echo "CSS exceeds the budget"
      exit 1
    fi

// ============================================
// PART 7: THE PR COMMENT
// ============================================

- name: Comment the size
  uses: actions/github-script@v7
  with:
    script: |
      const size = require('fs').statSync('dist/assets/index.css').size;
      github.rest.issues.createComment({
        issue_number: context.issue.number,
        owner: context.repo.owner,
        repo: context.repo.repo,
        body: `CSS size: ${size} bytes`,
      });

// ============================================
// PART 8: THE @APPLY AUDIT
// ============================================

# Find the @apply rules
grep -r "@apply" src/

# Find the usage
grep -r "btn" src/components/

// ============================================
// PART 9: THE CUSTOM UTILITY AUDIT
// ============================================

# Find the @utility registrations
grep -r "@utility" src/

# Find the usage
grep -r "tab-4" src/

// ============================================
// PART 10: THE COMPLETE PATTERN
// ============================================

// 1. The JIT removes the unused utilities.
// 2. The @apply and the @utility need the audit.
// 3. The safelist needs the periodic review.
// 4. The size-limit enforces the budget.
// 5. The bundlewatch comments on the PR.
// 6. The CI runs the audit and the budget.
// 7. The failure blocks the merge.
// 8. The comment makes the size visible.
// 9. The budget is the number in the config.
// 10. The gradual growth is the caught.

The ten parts showed the size-limit config, the bundlewatch config, the full CI workflow, the audit step, the audit script, the budget check, the PR comment, the @apply audit, the custom utility audit, and the complete pattern.


Quick Reference

The Budget Tools

The toolThe purpose
size-limitThe local budget
bundlewatchThe GitHub integration
The custom scriptThe audit
The CI workflowThe enforcement

The Audit Targets

The targetThe tool
The class usageThe tailwindcss-classnames
The @applyThe grep
The @utilityThe grep
The safelistThe grep

The Measurements

The measurementThe purpose
The raw sizeThe local
The gzipThe transfer
The brotliThe transfer
The budgetThe threshold

The CI Steps

The stepThe purpose
The lintThe code style
The testThe correctness
The buildThe output
The auditThe unused
The size-limitThe budget
The commentThe visibility

The Budget Config

The fieldThe purpose
pathThe glob
limitThe threshold
gzipThe compression
maxSizeThe bundlewatch

Best Practices

✅ Do This:

// Use the size-limit.
{
  "size-limit": [
    { "path": "dist/assets/*.css", "limit": "50 KB", "gzip": true }
  ]
}  // ✅
# Run the size-limit in the CI.
- run: npx size-limit  // ✅
# Comment the size on the PR.
- uses: actions/github-script@v7  // ✅
# Audit the @apply rules.
grep -r "@apply" src/  // ✅
# Audit the custom utilities.
grep -r "@utility" src/  // ✅
// Fail the CI on the unused.
if (unused.length > 0) process.exit(1);  // ✅

❌ Don’t Do This:

# Don't skip the audit.
- run: npm run build  // ❌ No budget
// Don't set the budget too high.
{ "limit": "1 MB" }  // ❌ The budget is the point
# Don't leave the dead @apply.
.btn { @apply bg-blue-600; }  // ❌ The btn is unused
# Don't leave the safelist forever.
safelist: ['text-red-600']  // ❌ The reason is gone
# Don't fail the build without the message.
exit 1  // ❌ The author does not know why
// Don't measure the raw size only.
const size = statSync('dist/index.css').size;  // ⚠️ The gzip matters

Common Pitfalls

PitfallWhy It HappensFix
The budget exceededThe growthOptimize or justify
The audit failsThe unused classesRemove them
The @apply deadThe rule unusedRemove the rule
The safelist staleThe reason goneRemove the entry
The CI slowThe full buildCache the deps
The comment missingNo scriptAdd the script
The measurement wrongThe raw sizeMeasure the gzip

Real-World Examples

1. The size-limit Config

{ "size-limit": [{ "path": "dist/*.css", "limit": "50 KB", "gzip": true }] }

2. The bundlewatch Config

{ "files": [{ "path": "./dist/*.css", "maxSize": "50 kB" }] }

3. The CI Workflow

- run: npm run build
- run: npx size-limit

4. The Audit Script

const unused = report.filter((c) => c.usage === 0);
if (unused.length) process.exit(1);

5. The Budget Check

CSS_SIZE=$(gzip -c dist/*.css | wc -c)

6. The PR Comment

- uses: actions/github-script@v7

7. The @apply Audit

grep -r "@apply" src/

8. The Custom Utility Audit

grep -r "@utility" src/

9. The Safelist Audit

grep -r "@source inline" src/

10. The Complete Pattern

- run: npm run build
- run: npx size-limit

Visual

The CI Pipeline

┌────────────────────────────────────────────────────────────┐
│  THE CI PIPELINE                                           │
│                                                            │
│  ┌──────────────────────────────────────────────────┐     │
│  │  THE LINT                                          │     │
│  │    The eslint, the prettier, the stylelint.        │     │
│  └────────────────────────┬─────────────────────────┘     │
│                           │                                │
│                           v                                │
│  ┌──────────────────────────────────────────────────┐     │
│  │  THE TEST                                          │     │
│  │    The unit, the integration.                      │     │
│  └────────────────────────┬─────────────────────────┘     │
│                           │                                │
│                           v                                │
│  ┌──────────────────────────────────────────────────┐     │
│  │  THE BUILD                                         │     │
│  │    The vite, the tailwind.                         │     │
│  └────────────────────────┬─────────────────────────┘     │
│                           │                                │
│                           v                                │
│  ┌──────────────────────────────────────────────────┐     │
│  │  THE AUDIT                                         │     │
│  │    The unused classes.                             │     │
│  └────────────────────────┬─────────────────────────┘     │
│                           │                                │
│                           v                                │
│  ┌──────────────────────────────────────────────────┐     │
│  │  THE SIZE-LIMIT                                    │     │
│  │    The budget check.                               │     │
│  └────────────────────────┬─────────────────────────┘     │
│                           │                                │
│                           v                                │
│  ┌──────────────────────────────────────────────────┐     │
│  │  THE COMMENT                                       │     │
│  │    The PR comment.                                 │     │
│  └──────────────────────────────────────────────────┘     │
└────────────────────────────────────────────────────────────┘

The Budget

┌────────────────────────────────────────────────────────────┐
│  THE BUDGET                                                │
│                                                            │
│  THE CONFIG                                                │
│  ┌──────────────────────────────────────────────────┐     │
│  │  size-limit: [                                     │     │
│  │    { "path": "dist/*.css",                         │     │
│  │      "limit": "50 KB",                             │     │
│  │      "gzip": true }                                │     │
│  │  ]                                                │     │
│  └──────────────────────────────────────────────────┘     │
│                                                            │
│  THE MEASUREMENT                                           │
│  ┌──────────────────────────────────────────────────┐     │
│  │  The raw: 180 KB                                   │     │
│  │  The gzip: 42 KB                                   │     │
│  │  The brotli: 38 KB                                 │     │
│  └──────────────────────────────────────────────────┘     │
│                                                            │
│  THE CHECK                                                 │
│  ┌──────────────────────────────────────────────────┐     │
│  │  42 KB < 50 KB → the pass                          │     │
│  │  55 KB > 50 KB → the fail                          │     │
│  └──────────────────────────────────────────────────┘     │
└────────────────────────────────────────────────────────────┘

The Audit Targets

┌────────────────────────────────────────────────────────────┐
│  THE AUDIT TARGETS                                         │
│                                                            │
│  THE @apply                                                │
│  ┌──────────────────────────────────────────────────┐     │
│  │  .btn { @apply bg-blue-600 px-4; }                 │     │
│  │    → the btn is used?                              │     │
│  │    → the grep the usage                            │     │
│  └──────────────────────────────────────────────────┘     │
│                                                            │
│  THE @utility                                              │
│  ┌──────────────────────────────────────────────────┐     │
│  │  @utility tab-4 { tab-size: 4; }                   │     │
│  │    → the tab-4 is used?                            │     │
│  │    → the grep the markup                           │     │
│  └──────────────────────────────────────────────────┘     │
│                                                            │
│  THE SAFELIST                                              │
│  ┌──────────────────────────────────────────────────┐     │
│  │  @source inline("text-red-600");                   │     │
│  │    → the reason still valid?                       │     │
│  │    → the review the entry                          │     │
│  └──────────────────────────────────────────────────┘     │
└────────────────────────────────────────────────────────────┘

The Gradual Growth

┌────────────────────────────────────────────────────────────┐
│  THE GRADUAL GROWTH                                        │
│                                                            │
│  THE LAUNCH                                                │
│  ┌──────────────────────────────────────────────────┐     │
│  │  The CSS: 48 KB                                    │     │
│  └──────────────────────────────────────────────────┘     │
│                                                            │
│  THE 6 MONTHS                                              │
│  ┌──────────────────────────────────────────────────┐     │
│  │  The CSS: 62 KB                                    │     │
│  │    → the budget check: the pass (62 < 100)         │     │
│  └──────────────────────────────────────────────────┘     │
│                                                            │
│  THE 12 MONTHS                                             │
│  ┌──────────────────────────────────────────────────┐     │
│  │  The CSS: 95 KB                                    │     │
│  │    → the budget check: the pass (95 < 100)         │     │
│  └──────────────────────────────────────────────────┘     │
│                                                            │
│  THE 18 MONTHS                                             │
│  ┌──────────────────────────────────────────────────┐     │
│  │  The CSS: 120 KB                                   │     │
│  │    → the budget check: the FAIL                    │     │
│  │    → the author must justify or optimize           │     │
│  └──────────────────────────────────────────────────┘     │
└────────────────────────────────────────────────────────────┘

Summary

ItemValue
The JITThe removes the unused utilities
The @applyThe audit the rule
The @utilityThe audit the custom
The safelistThe periodic review
size-limitThe budget tool
bundlewatchThe GitHub integration
The gzip sizeThe transfer measurement
The budgetThe threshold
The CI workflowThe enforcement
The PR commentThe visibility

Key takeaways:

  • The Tailwind JIT already removes the unused utilities. The class that is not in the source is not in the output. The audit is about the classes the engine cannot see: the @apply, the @utility, and the safelist.
  • The @apply rules are the source of the dead CSS. The rule that is no longer used remains until the removal. The grep finds the rules, and the usage check finds the dead.
  • The @utility registrations are the custom utilities. The engine generates the utility because the @utility registers it. The markup usage check finds the dead.
  • The safelist needs the periodic review. The entry that was the workaround remains after the refactor. The reason should be the documented, and the entry removed when the reason is gone.
  • The size-limit is the budget tool. The config lists the paths, the limits, and the compression. The CI runs the tool, and the failure blocks the merge.
  • The bundlewatch adds the GitHub integration. The tool compares with the base branch and the comments on the pull request. The size change is the visible.
  • The gzip size is the transfer measurement. The raw size is the local, and the gzip and the brotli are the transfer. The budget should be on the transfer size.
  • The gradual growth is the caught. The budget is the threshold, and the CI fails when it is exceeded. The author must justify the increase or the optimize the change.

Remember: The unused-class auditing and the CSS bundle size budgeting are the CI/CD discipline. The Tailwind JIT removes the unused utilities, and the audit targets the @apply, the @utility, and the safelist. The size-limit enforces the budget, and the bundlewatch makes the size visible on the pull request. The result is the bundle that stays within the threshold, the regression caught at the review, and the gradual growth the addressed.


Was this helpful?


Stop using slow, ad-bloated tool sites! 🤮

🔎 Search “KandZ Tools” on Google to use many professional utilities for free.

KandZ.me is the ultimate minimalist hub for:
✅ Finance (Mortgage, Interest, Inflation)
✅ Tech (Base64, JSON, Dev Suite, IP)
✅ Health (BMI, BMR, TDEE)
✅ Productivity (Timer, Workspace, QR)

⚡️ Fast & Private
🔒 No data leaves your device
💎 100% Free

🔗 Use it now: https://tools.kandz.me
🔖 Bookmark it—you’ll need it later!