Why CSS Performance Matters

CSS is render-blocking — browsers won’t paint until CSSOM is built. Large stylesheets delay First Contentful Paint (FCP), Largest Contentful Paint (LCP), and can cause Cumulative Layout Shift (CLS). Optimized CSS means faster, smoother pages and better Core Web Vitals scores.

Measure First

Before optimizing, establish baselines:

  • Chrome DevTools → Coverage — unused CSS percentage
  • Lighthouse — performance score, render-blocking resources
  • WebPageTest — waterfall, start render time

Minimize and Compress

  # Production minification
npx postcss styles.css --use cssnano -o styles.min.css
  
  • Remove unused CSS with PurgeCSS, Tailwind JIT, or @layer pruning
  • Enable Brotli (preferred) or gzip compression on the server
  • Split critical vs non-critical CSS

Typical savings: 40–70% file size reduction from minification + compression.

Critical CSS

Inline above-the-fold styles in <head>, defer the rest:

  <head>
    <style>
        /* Critical: header, hero, layout skeleton */
        body { margin: 0; font-family: system-ui, sans-serif; }
        .hero { min-height: 100vh; display: flex; align-items: center; }
        .nav { display: flex; justify-content: space-between; padding: 1rem; }
    </style>
    <link rel="preload" href="/css/main.css" as="style" onload="this.onload=null;this.rel='stylesheet'">
    <noscript><link rel="stylesheet" href="/css/main.css"></noscript>
</head>
  

Tools: Critical, Vite’s CSS code splitting, Penthouse.

Reduce Unused CSS

  // tailwind.config.js — content paths for purge
module.exports = {
    content: ['./src/**/*.{html,js,jsx,tsx,vue}'],
};
  

Audit component libraries — importing entire Bootstrap when you use 5 classes wastes bytes.

Selector Efficiency

Browsers match selectors right to left. Keep selectors shallow:

  /* Slow — deep nesting, many candidates checked */
.sidebar nav ul li a span { }

/* Fast — single class */
.nav-link { }
  

Avoid universal selectors in complex rules. ID selectors (#header) create high specificity and slow matching — prefer classes.

Avoid Expensive Properties

Some properties trigger layout (reflow), paint, or composite:

Expensive Cheaper Alternative
Animating width, height, top, left Animate transform, opacity
box-shadow on hundreds of elements Use sparingly; simplify shadow
filter: blur() on large areas Pre-render blurred images
position: fixed with heavy content Virtualize long lists
  .card {
    transition: transform 0.2s ease, opacity 0.2s ease;
    will-change: transform; /* use sparingly — memory cost */
}
.card:hover {
    transform: translateY(-4px);  /* compositor-only */
}
  

Only animate properties that trigger composite layer: transform, opacity.

Reduce Repaints and Reflows

  • Batch DOM reads/writes in JavaScript (avoid layout thrashing)
  • Use content-visibility: auto for off-screen sections:
  .section-below-fold {
    content-visibility: auto;
    contain-intrinsic-size: 0 500px;
}
  
  • Limit @font-face variants — each file is a network request
  • Use font-display: swap to prevent invisible text

Containment

  .card {
    contain: layout style paint;
}
  

Tells browser that internal changes don’t affect outside layout — enables optimization.

Audit Checklist

  • CSS minified and compressed in production
  • Unused CSS removed (< 20% unused in Coverage tab)
  • Critical CSS inlined or preloaded
  • No render-blocking CSS beyond critical path
  • Animations use transform/opacity only
  • Font files subset and preloaded

Troubleshooting

High CLS after CSS loads

  • Reserve space for images, ads, embeds with explicit dimensions
  • Avoid injecting styles that shift layout

Slow FCP on mobile

  • Reduce CSS payload under 50KB compressed for critical path
  • Eliminate @import chains — use <link> tags

Janky scroll animations

  • Add { passive: true } to scroll listeners
  • Use will-change only during animation, remove after

CSS performance is about shipping less, loading smarter, and animating on the compositor thread.