CSS Performance
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
@layerpruning - 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: autofor off-screen sections:
.section-below-fold {
content-visibility: auto;
contain-intrinsic-size: 0 500px;
}
- Limit
@font-facevariants — each file is a network request - Use
font-display: swapto 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/opacityonly - 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
@importchains — use<link>tags
Janky scroll animations
- Add
{ passive: true }to scroll listeners - Use
will-changeonly during animation, remove after
CSS performance is about shipping less, loading smarter, and animating on the compositor thread.