CSS Performance Optimization
1. CSS Selector Performance and Efficiency
| Selector Type | Performance | Reason | Example |
|---|---|---|---|
| ID Selector | Fastest | Direct hash lookup | #header { } |
| Class Selector | Very Fast | Indexed by browser | .button { } |
| Tag Selector | Fast | Grouped by element type | div { } |
| Pseudo-class | Moderate | Requires state checking | :hover, :focus |
| Attribute Selector | Moderate | Must scan attributes | [type="text"] |
| Universal Selector | Slow | Matches all elements | * { } |
| Complex Selector | Slower | Right-to-left matching | .nav li a span |
| :has() Selector | Slowest | Must check descendants | div:has(> p) |
Example: Selector efficiency comparison
/* FAST - Use specific classes */
.header { }
.nav-item { }
.button-primary { }
/* SLOWER - Avoid deep nesting */
.header nav ul li a span {
/* Browser reads right-to-left:
1. Find all span elements
2. Check if inside a
3. Check if inside li
4. Check if inside ul
5. Check if inside nav
6. Check if inside .header */
}
/* BETTER - Flatten with specific class */
.nav-link-text { }
/* SLOW - Universal selector */
* {
box-sizing: border-box; /* OK for reset, avoid elsewhere */
}
.widget * {
/* Matches EVERY descendant - expensive */
}
/* SLOWER - Unqualified attribute selector */
[type="text"] {
/* Must check every element for type attribute */
}
/* BETTER - Qualify with tag */
input[type="text"] {
/* Only checks input elements */
}
/* SLOW - Over-specific */
div#header div.nav ul li a.link {
/* Unnecessary specificity */
}
/* FAST - Use BEM or single class */
.header-nav-link { }
/* Modern :has() - use sparingly */
article:has(img) {
/* Finds articles containing images - expensive */
}
/* Performance tips:
1. Use classes over tags + classes: .button not button.button
2. Avoid universal selector *
3. Avoid over-qualifying: .class not div.class
4. Keep nesting shallow (1-3 levels max)
5. Use specific classes instead of descendant selectors
6. Browsers optimize right-to-left matching
*/
Example: Selector optimization patterns
/* ❌ BAD: Deep nesting, over-qualified */
body div.container section#main article.post div.content p.text span.highlight {
color: yellow;
}
/* ✅ GOOD: Single specific class */
.text-highlight {
color: yellow;
}
/* ❌ BAD: Universal child selector */
.sidebar * {
margin: 0;
}
/* ✅ GOOD: Specific elements */
.sidebar h3,
.sidebar p,
.sidebar ul {
margin: 0;
}
/* ❌ BAD: Attribute selector alone */
[data-active="true"] {
background: blue;
}
/* ✅ GOOD: Qualified attribute selector */
.tab[data-active="true"] {
background: blue;
}
/* ❌ BAD: Over-qualified */
ul.nav li.nav-item a.nav-link {
/* Unnecessary tag selectors */
}
/* ✅ GOOD: BEM classes */
.nav__item-link {
/* Single class, clear semantics */
}
/* ❌ BAD: Complex pseudo-class chains */
.menu li:first-child:last-child:only-child a:hover:focus {
/* Too complex */
}
/* ✅ GOOD: Simpler, specific */
.menu-single-item-link:hover,
.menu-single-item-link:focus {
/* More readable, equally specific */
}
/* Modern optimization: use :is() to reduce specificity */
/* ❌ BAD: High specificity */
.header nav ul li a { }
.footer nav ul li a { }
.sidebar nav ul li a { }
/* ✅ GOOD: Lower specificity with :is() */
:is(.header, .footer, .sidebar) .nav-link { }
/* Use :where() for zero specificity */
:where(.header, .footer) .nav-link {
/* Specificity: (0,1,0) - just .nav-link */
}
/* Performance measurement */
/* Use browser DevTools:
- Chrome: Performance tab → Recalculate Style
- Firefox: Performance tool → Styles
- Look for selector matching time
*/
Note: Modern browsers are very fast at selector matching. Optimize for maintainability first, then performance. Avoid over-nesting (3+ levels), universal
selectors in large scopes, and unqualified attribute selectors. Use
:is() and :where()
to simplify complex selectors.
2. CSS Containment (contain property) for Performance
| Containment Type | Property Value | What It Does | Use Case |
|---|---|---|---|
| Layout Containment | contain: layout |
Isolates layout calculations | Independent widgets, cards |
| Paint Containment | contain: paint |
Limits painting to bounds | Components with overflow |
| Size Containment | contain: size |
Ignores children for sizing | Fixed-size containers |
| Style Containment | contain: style |
Isolates counter/quote styles | Rare, specific cases |
| Content Containment | contain: content |
layout + paint + style | Independent components |
| Strict Containment | contain: strict |
All containment types | Maximum isolation |
| Inline Size | contain: inline-size |
Contains inline axis only | Container queries |
| Content Visibility | content-visibility: auto |
Skip rendering off-screen | Long pages, lists |
Example: CSS containment implementation
/* Layout containment: isolate layout */
.card {
contain: layout;
/* Layout changes inside .card don't affect outside */
/* float, clear, margin collapse isolated */
}
/* Paint containment: clip to bounds */
.widget {
contain: paint;
/* Content can't paint outside widget bounds */
/* Creates stacking context */
/* overflow clipping applied */
}
/* Size containment: ignore children */
.fixed-container {
contain: size;
width: 300px;
height: 400px;
/* Size is fixed, children don't affect it */
/* MUST specify dimensions */
}
/* Content containment: layout + paint + style */
.component {
contain: content;
/* Most common for independent components */
}
/* Strict containment: maximum isolation */
.isolated-widget {
contain: strict; /* size + layout + paint + style */
width: 400px;
height: 300px;
/* Completely isolated from rest of page */
}
/* Inline-size containment: for container queries */
.container {
contain: inline-size layout style;
container-type: inline-size;
/* Required for container queries */
}
/* Combined containment */
.card-list-item {
contain: layout paint;
/* Most practical combination */
/* No size requirement */
}
/* Content-visibility: automatic rendering */
.article-section {
content-visibility: auto;
contain-intrinsic-size: auto 500px;
/* Browser skips rendering until near viewport */
/* Massive performance boost for long pages */
}
/* Long list optimization */
.list-item {
content-visibility: auto;
contain-intrinsic-size: auto 100px;
}
/* Nested containment */
.page-section {
contain: layout paint;
}
.page-section .card {
contain: layout paint;
/* Further isolation */
}
/* Performance impact:
- 30-50% faster layout for complex pages
- Reduced paint time
- Better scroll performance
- Lower memory usage
*/
Example: Content-visibility with intersection observer
/* CSS: Skip rendering off-screen sections */
.article-section {
content-visibility: auto;
contain-intrinsic-size: auto 800px; /* Estimated height */
}
.comment-section {
content-visibility: auto;
contain-intrinsic-size: auto 600px;
}
/* More control with hidden/visible */
.below-fold {
content-visibility: hidden; /* Always skip */
}
.above-fold {
content-visibility: visible; /* Always render */
}
/* JavaScript: Toggle rendering based on visibility */
/*
const observer = new IntersectionObserver((entries) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
entry.target.style.contentVisibility = 'visible';
} else {
entry.target.style.contentVisibility = 'hidden';
}
});
}, {
rootMargin: '100px' // Start rendering 100px before visible
});
document.querySelectorAll('.lazy-section').forEach(section => {
observer.observe(section);
});
*/
/* Contain-intrinsic-size: preserve layout space */
.lazy-section {
content-visibility: auto;
/* Multiple values for width and height */
contain-intrinsic-size: 1000px 800px;
}
/* Auto with fallback */
.dynamic-section {
content-visibility: auto;
contain-intrinsic-size: auto 500px;
/* auto uses last rendered size, 500px is initial */
}
/* Performance benefits:
- 50-90% faster initial render on long pages
- Reduced memory usage
- Smoother scrolling
- Faster time-to-interactive
*/
/* When to use:
- Long articles with many sections
- Infinite scroll lists
- Tabbed content (hidden tabs)
- Collapsed accordions
- Off-screen modals
*/
Warning:
contain: size and contain: strict require explicit
dimensions. content-visibility: auto can cause scrollbar jumping if
contain-intrinsic-size estimate is wrong. Test with DevTools Performance tab to measure impact.
3. will-change Property Optimization
| will-change Value | Optimization | Cost | Best Practice |
|---|---|---|---|
transform |
Creates GPU layer | High memory | Use before animation, remove after |
opacity |
GPU compositing | Medium memory | For fade animations |
scroll-position |
Optimize scrolling | Low | For scroll-driven effects |
contents |
General optimization hint | Variable | When specific property unknown |
| Multiple properties | will-change: transform, opacity |
Very high | Avoid, use sparingly |
auto |
No special optimization | None | Default, remove hint |
Example: Correct will-change usage
/* ❌ BAD: Don't apply to many elements */
* {
will-change: transform; /* Memory explosion! */
}
/* ❌ BAD: Don't apply in stylesheet permanently */
.element {
will-change: transform; /* Always uses extra memory */
}
/* ✅ GOOD: Apply just before animation */
.element {
transition: transform 0.3s;
}
.element:hover {
will-change: transform; /* Hint applied on hover */
transform: scale(1.1);
}
/* ✅ BETTER: Add/remove with JavaScript */
/*
const element = document.querySelector('.animate-me');
// Before animation
element.style.willChange = 'transform';
// Start animation
element.classList.add('animated');
// After animation completes (listen to transitionend)
element.addEventListener('transitionend', () => {
element.style.willChange = 'auto'; // Remove hint
});
*/
/* Practical pattern: Modal animation */
.modal {
opacity: 0;
transform: translateY(-50px);
transition: opacity 0.3s, transform 0.3s;
}
.modal.opening {
will-change: opacity, transform;
/* Add before showing */
}
.modal.visible {
opacity: 1;
transform: translateY(0);
}
.modal.closing {
will-change: auto; /* Remove optimization */
}
/* Scroll-driven animation */
.parallax-section {
will-change: transform;
/* OK for scrolling elements */
}
/* When element leaves viewport */
/*
const observer = new IntersectionObserver((entries) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
entry.target.style.willChange = 'transform';
} else {
entry.target.style.willChange = 'auto';
}
});
});
*/
/* GPU-accelerated properties only */
.gpu-animated {
/* These are GPU-accelerated: */
will-change: transform; /* ✅ */
will-change: opacity; /* ✅ */
will-change: filter; /* ✅ */
/* These are NOT GPU-accelerated: */
will-change: width; /* ❌ Causes reflow */
will-change: height; /* ❌ Causes reflow */
will-change: margin; /* ❌ Causes reflow */
will-change: color; /* ❌ Doesn't benefit */
}
/* Best practice: Transform + opacity only */
.smooth-animation {
will-change: transform, opacity;
/* Most common combination */
}
/* Memory usage comparison:
No will-change: 0 MB extra
will-change: transform on 10 elements: ~5-10 MB
will-change: transform on 1000 elements: ~500+ MB
*/
Example: will-change lifecycle management
/* JavaScript pattern: Add before, remove after */
/*
class AnimationOptimizer {
constructor(element) {
this.element = element;
this.properties = [];
}
// Add optimization hint
optimize(properties) {
this.properties = Array.isArray(properties) ? properties : [properties];
this.element.style.willChange = this.properties.join(', ');
}
// Remove optimization hint
deoptimize() {
this.element.style.willChange = 'auto';
this.properties = [];
}
// Animate with automatic cleanup
async animate(animation, duration) {
// Apply hint before animation
this.optimize(['transform', 'opacity']);
// Wait a frame for optimization to apply
await new Promise(resolve => requestAnimationFrame(resolve));
// Start animation
this.element.animate(animation, { duration });
// Remove hint after animation
setTimeout(() => this.deoptimize(), duration);
}
}
// Usage
const optimizer = new AnimationOptimizer(document.querySelector('.card'));
// Before hover animation
card.addEventListener('mouseenter', () => {
optimizer.optimize('transform');
});
card.addEventListener('mouseleave', () => {
optimizer.deoptimize();
});
// For FLIP animations
function flipAnimate(element, newState) {
// First: Record current position
const first = element.getBoundingClientRect();
// Last: Apply new state
element.classList.add(newState);
const last = element.getBoundingClientRect();
// Invert: Calculate difference
const deltaX = first.left - last.left;
const deltaY = first.top - last.top;
// Play: Animate from inverted position
element.style.willChange = 'transform';
element.style.transform = `translate(${deltaX}px, ${deltaY}px)`;
requestAnimationFrame(() => {
element.style.transition = 'transform 0.3s';
element.style.transform = 'none';
element.addEventListener('transitionend', () => {
element.style.willChange = 'auto';
element.style.transition = '';
}, { once: true });
});
}
*/
/* CSS-only pattern with interaction states */
.interactive-card {
transition: transform 0.3s ease;
}
/* Apply hint on any interaction */
.interactive-card:hover,
.interactive-card:focus,
.interactive-card:active {
will-change: transform;
}
/* Remove hint when interaction ends (auto after animation) */
.interactive-card {
/* will-change automatically cleared after transition */
}
/* For continuous animations (use carefully) */
@keyframes float {
0%, 100% { transform: translateY(0); }
50% { transform: translateY(-20px); }
}
.floating-element {
will-change: transform; /* OK for continuous animation */
animation: float 3s ease-in-out infinite;
}
/* But better to scope to visible elements only */
.floating-element {
animation: float 3s ease-in-out infinite;
}
.floating-element.in-viewport {
will-change: transform; /* Only when visible */
}
/* Performance monitoring */
/*
// Measure memory impact
console.memory.usedJSHeapSize // Before
element.style.willChange = 'transform';
console.memory.usedJSHeapSize // After (Chrome only)
*/
Note:
will-change creates GPU layers consuming memory. Only use for transform and opacity animations. Apply just before
animation, remove after. Never apply to many elements or permanently in CSS. Use JavaScript for lifecycle
management.
4. CSS Loading Strategies (preload, prefetch)
| Strategy | Technique | Performance Impact | Use Case |
|---|---|---|---|
| Inline Critical CSS | Embed in <style> tag | Eliminates render-blocking request | Above-fold styles (<14KB) |
| Preload | <link rel="preload"> |
Early resource discovery | Critical CSS files |
| Async CSS | Load non-blocking | Faster initial render | Below-fold styles |
| Defer CSS | Load after page load | Fastest FCP | Non-critical styles |
| Code Splitting | Separate CSS by route | Smaller initial bundle | Multi-page apps |
| HTTP/2 Push | Server-side push | Eliminate round-trip | Known critical resources |
| Lazy Load | Load on demand | Reduced initial load | Component-specific CSS |
Example: CSS loading strategies
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<!-- 1. Inline critical CSS (above-fold only) -->
<style>
/* Critical styles: header, hero, initial viewport */
body { margin: 0; font-family: system-ui; }
.header { background: #fff; padding: 1rem; }
.hero { min-height: 60vh; display: flex; }
/* Keep under 14KB for optimal performance */
</style>
<!-- 2. Preload critical CSS -->
<link rel="preload" href="/css/critical.css" as="style">
<!-- 3. Preload fonts (if critical) -->
<link rel="preload" href="/fonts/inter.woff2" as="font" type="font/woff2" crossorigin>
<!-- 4. Async load main CSS -->
<link rel="stylesheet" href="/css/main.css" media="print" onload="this.media='all'">
<noscript><link rel="stylesheet" href="/css/main.css"></noscript>
<!-- 5. Preconnect to external domains -->
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<!-- 6. DNS prefetch for lower priority domains -->
<link rel="dns-prefetch" href="https://analytics.example.com">
</head>
<body>
<!-- Content -->
<!-- 7. Lazy load non-critical CSS at end of body -->
<script>
// Load below-fold CSS after page load
window.addEventListener('load', () => {
const link = document.createElement('link');
link.rel = 'stylesheet';
link.href = '/css/below-fold.css';
document.head.appendChild(link);
});
// Or defer to idle time
if ('requestIdleCallback' in window) {
requestIdleCallback(() => {
const link = document.createElement('link');
link.rel = 'stylesheet';
link.href = '/css/enhancements.css';
document.head.appendChild(link);
});
}
</script>
</body>
</html>
Example: Route-based code splitting
/* Webpack configuration for CSS splitting */
/*
module.exports = {
entry: {
home: './src/pages/home.js',
about: './src/pages/about.js',
products: './src/pages/products.js',
},
output: {
filename: '[name].[contenthash].js',
},
plugins: [
new MiniCssExtractPlugin({
filename: '[name].[contenthash].css',
}),
],
optimization: {
splitChunks: {
cacheGroups: {
// Common CSS shared across routes
commons: {
name: 'commons',
chunks: 'all',
minChunks: 2,
priority: 10,
reuseExistingChunk: true,
enforce: true,
},
// Vendor CSS (from node_modules)
vendor: {
test: /[\\/]node_modules[\\/]/,
name: 'vendor',
chunks: 'all',
priority: 20,
},
},
},
},
};
*/
/* HTML: Load only required CSS per page */
/*
<!-- Home page -->
<link rel="stylesheet" href="/css/commons.css">
<link rel="stylesheet" href="/css/home.css">
<!-- About page -->
<link rel="stylesheet" href="/css/commons.css">
<link rel="stylesheet" href="/css/about.css">
<!-- Products page -->
<link rel="stylesheet" href="/css/commons.css">
<link rel="stylesheet" href="/css/vendor.css">
<link rel="stylesheet" href="/css/products.css">
*/
/* Dynamic import for lazy loading */
/*
// Load CSS when component mounts
async function loadComponentStyles() {
if (!document.querySelector('link[href*="modal.css"]')) {
const link = document.createElement('link');
link.rel = 'stylesheet';
link.href = '/css/components/modal.css';
document.head.appendChild(link);
// Wait for CSS to load
await new Promise(resolve => {
link.onload = resolve;
});
}
}
// Usage in React/Vue component
useEffect(() => {
loadComponentStyles();
}, []);
*/
/* HTTP/2 Server Push (server config) */
/*
// Node.js/Express example
app.get('/', (req, res) => {
// Push critical CSS
res.push('/css/critical.css', {
response: { 'content-type': 'text/css' }
});
res.sendFile('index.html');
});
*/
/* Service Worker caching strategy */
/*
// cache-first for CSS
self.addEventListener('fetch', event => {
if (event.request.destination === 'style') {
event.respondWith(
caches.match(event.request).then(response => {
return response || fetch(event.request).then(fetchResponse => {
return caches.open('css-v1').then(cache => {
cache.put(event.request, fetchResponse.clone());
return fetchResponse;
});
});
})
);
}
});
*/
Warning: Preload only 1-2 critical resources to avoid bandwidth contention. Inline CSS
increases HTML size. HTTP/2 push can cause over-pushing if not careful. Test with Lighthouse and WebPageTest for
real-world impact.
5. Reducing Layout Thrashing and Reflows
| Issue | Cause | Impact | Solution |
|---|---|---|---|
| Layout Thrashing | Read then write DOM in loop | Forced synchronous layouts | Batch reads, then batch writes |
| Reflow | Geometry changes (width, height) | Entire layout recalculation | Use transform instead |
| Repaint | Visual changes (color, visibility) | Pixel repaint | Less expensive than reflow |
| Composite | GPU layer changes | Cheapest, GPU-accelerated | Use transform, opacity only |
Example: Avoid layout thrashing
/* ❌ BAD: Causes layout thrashing */
/*
const elements = document.querySelectorAll('.item');
elements.forEach(el => {
// READ (triggers layout)
const height = el.offsetHeight;
// WRITE (invalidates layout)
el.style.height = (height + 10) + 'px';
// Browser must recalculate layout on each iteration!
});
*/
/* ✅ GOOD: Batch reads, then batch writes */
/*
const elements = document.querySelectorAll('.item');
// First: Batch all reads
const heights = Array.from(elements).map(el => el.offsetHeight);
// Then: Batch all writes
elements.forEach((el, i) => {
el.style.height = (heights[i] + 10) + 'px';
});
*/
/* ❌ BAD: Reading layout properties in animation */
/*
function animate() {
const box = document.querySelector('.box');
// Triggers layout
const width = box.offsetWidth;
box.style.width = (width + 1) + 'px';
requestAnimationFrame(animate);
}
*/
/* ✅ GOOD: Use CSS transform (no layout) */
/*
let scale = 1;
function animate() {
scale += 0.01;
box.style.transform = `scaleX(${scale})`;
requestAnimationFrame(animate);
}
*/
/* Properties that trigger reflow (EXPENSIVE): */
/*
el.offsetHeight, el.offsetWidth, el.offsetTop, el.offsetLeft
el.clientHeight, el.clientWidth, el.clientTop, el.clientLeft
el.scrollHeight, el.scrollWidth, el.scrollTop, el.scrollLeft
el.getBoundingClientRect()
window.getComputedStyle()
// Setting these properties causes reflow:
width, height, margin, padding, border
top, right, bottom, left
font-size, line-height
display, position, float
*/
/* Properties that only repaint (MODERATE): */
/*
color, background, background-color
border-color, box-shadow
visibility, outline
*/
/* Properties that only composite (FAST): */
/*
transform, opacity
*/
/* Use transform instead of position changes */
/* ❌ SLOW: Causes reflow */
.box {
position: absolute;
left: 100px;
transition: left 0.3s;
}
.box:hover {
left: 200px; /* Reflow! */
}
/* ✅ FAST: GPU compositing */
.box {
transform: translateX(0);
transition: transform 0.3s;
}
.box:hover {
transform: translateX(100px); /* No reflow! */
}
Example: Layout optimization techniques
/* Use CSS custom properties to batch updates */
:root {
--item-width: 100px;
}
.item {
width: var(--item-width);
}
/* Update once, affects all items */
/*
document.documentElement.style.setProperty('--item-width', '200px');
*/
/* Use DocumentFragment for batch DOM insertions */
/*
const fragment = document.createDocumentFragment();
for (let i = 0; i < 100; i++) {
const item = document.createElement('div');
item.textContent = `Item ${i}`;
fragment.appendChild(item);
}
// Single reflow instead of 100
container.appendChild(fragment);
*/
/* Use classList for multiple class changes */
/* ❌ BAD: Multiple reflows */
/*
el.className = 'box'; // Reflow
el.className = 'box active'; // Reflow
el.className = 'box active visible'; // Reflow
*/
/* ✅ GOOD: Single update */
/*
el.className = 'box active visible'; // One reflow
// Or use classList
el.classList.add('active', 'visible');
*/
/* Clone, modify, replace pattern */
/* ❌ BAD: Modifying visible element */
/*
const list = document.querySelector('.list');
for (let i = 0; i < 100; i++) {
const item = document.createElement('li');
item.textContent = `Item ${i}`;
list.appendChild(item); // 100 reflows
}
*/
/* ✅ GOOD: Modify detached element */
/*
const list = document.querySelector('.list');
const parent = list.parentNode;
const clone = list.cloneNode(false);
for (let i = 0; i < 100; i++) {
const item = document.createElement('li');
item.textContent = `Item ${i}`;
clone.appendChild(item);
}
parent.replaceChild(clone, list); // One reflow
*/
/* Hide element during batch modifications */
/* ❌ BAD: Visible modifications */
/*
el.style.width = '100px';
el.style.height = '100px';
el.style.margin = '10px';
*/
/* ✅ GOOD: Hide during modification */
/*
el.style.display = 'none'; // Reflow
el.style.width = '100px';
el.style.height = '100px';
el.style.margin = '10px';
el.style.display = 'block'; // Reflow
// Total: 2 reflows instead of 4
*/
/* Use CSS containment */
.contained {
contain: layout;
/* Changes inside don't affect outside */
}
/* requestAnimationFrame for visual updates */
/*
let updates = [];
function scheduleUpdate(element, property, value) {
updates.push({ element, property, value });
if (updates.length === 1) {
requestAnimationFrame(flushUpdates);
}
}
function flushUpdates() {
updates.forEach(({ element, property, value }) => {
element.style[property] = value;
});
updates = [];
}
// Usage
scheduleUpdate(box1, 'width', '200px');
scheduleUpdate(box2, 'height', '300px');
// Both applied in same frame
*/
Note: Transform and opacity are GPU-accelerated, causing no
layout/paint. Batch DOM reads then writes. Use
requestAnimationFrame for visual updates. Avoid
reading layout properties (offsetHeight, getBoundingClientRect) in loops or
animations.
6. CSS Bundle Optimization Techniques
| Technique | Tool/Method | Impact | Complexity |
|---|---|---|---|
| Remove Unused CSS | PurgeCSS, UnCSS | 50-90% size reduction | Medium |
| Minification | cssnano, clean-css | 20-30% reduction | Low |
| Compression | Gzip, Brotli | 70-80% reduction | Low (server config) |
| CSS Splitting | Webpack, Vite | Faster initial load | Medium |
| Deduplicate Rules | cssnano advanced | 5-15% reduction | Low |
| Shorthand Properties | cssnano, manual | 5-10% reduction | Low |
| Critical CSS Extraction | Critical, Critters | Faster FCP | Medium |
Example: PurgeCSS configuration
/* purgecss.config.js */
/*
module.exports = {
content: [
'./src/!**!/!*.html',
'./src/!**!/!*.js',
'./src/!**!/!*.jsx',
'./src/!**!/!*.vue',
],
css: ['./src/!**!/!*.css'],
output: './dist/css',
// Safe list: classes to never remove
safelist: [
'active',
'show',
'fade',
/^data-/, // Keep data-* attributes
/^js-/, // Keep js-* classes
],
// Safelist patterns
safelist: {
standard: ['active', 'show'],
deep: [/^modal/, /^tooltip/], // Keep all modal-*, tooltip-*
greedy: [/^data-/], // Keep anything with data-
},
// Keep all keyframe animations
keyframes: true,
// Keep font-face rules
fontFace: true,
// Variables to keep
variables: true,
// Custom extractors for dynamic classes
extractors: [
{
extractor: content => content.match(/[\w-/:]+(?<!:)/g) || [],
extensions: ['html', 'js', 'jsx']
}
]
};
*/
/* PostCSS integration */
/*
module.exports = {
plugins: [
require('@fullhuman/postcss-purgecss')({
content: ['./src/!**!/!*.html'],
defaultExtractor: content => {
// Extract classes including special characters
const broadMatches = content.match(/[^<>"'`\s]*[^<>"'`\s:]/g) || [];
const innerMatches = content.match(/[^<>"'`\s.()]*[^<>"'`\s.():]/g) || [];
return broadMatches.concat(innerMatches);
}
})
]
};
*/
/* Before PurgeCSS: 500KB CSS */
/* After PurgeCSS: 50KB CSS (90% reduction) */
Example: Build optimization pipeline
/* package.json scripts */
/*
{
"scripts": {
"css:build": "npm run css:compile && npm run css:optimize",
"css:compile": "sass src/scss:dist/css",
"css:optimize": "npm run css:prefix && npm run css:purge && npm run css:minify",
"css:prefix": "postcss dist/css/!*.css --use autoprefixer -d dist/css",
"css:purge": "purgecss --css dist/css/!*.css --content src/!**!/!*.html --output dist/css",
"css:minify": "cssnano dist/css/!*.css dist/css/!*.min.css"
}
}
*/
/* Webpack optimization config */
/*
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
const CssMinimizerPlugin = require('css-minimizer-webpack-plugin');
const { PurgeCSSPlugin } = require('purgecss-webpack-plugin');
const glob = require('glob');
const path = require('path');
module.exports = {
mode: 'production',
module: {
rules: [
{
test: /\.css$/,
use: [
MiniCssExtractPlugin.loader,
'css-loader',
'postcss-loader'
]
}
]
},
plugins: [
new MiniCssExtractPlugin({
filename: '[name].[contenthash].css'
}),
new PurgeCSSPlugin({
paths: glob.sync(`src/!**!/!*`, { nodir: true })
})
],
optimization: {
minimizer: [
new CssMinimizerPlugin({
minimizerOptions: {
preset: [
'advanced',
{
discardComments: { removeAll: true },
reduceIdents: true,
mergeRules: true,
mergeLonghand: true,
cssDeclarationSorter: true
}
]
}
})
],
splitChunks: {
cacheGroups: {
styles: {
name: 'styles',
type: 'css/mini-extract',
chunks: 'all',
enforce: true
}
}
}
}
};
*/
/* Vite optimization */
/*
// vite.config.js
import { defineConfig } from 'vite';
export default defineConfig({
build: {
cssMinify: 'lightningcss',
cssCodeSplit: true,
rollupOptions: {
output: {
assetFileNames: 'assets/[name].[hash][extname]',
manualChunks: {
vendor: ['node_modules']
}
}
}
},
css: {
devSourcemap: true,
preprocessorOptions: {
scss: {
additionalData: `@import "@/styles/variables.scss";`
}
}
}
});
*/
Example: Manual optimization techniques
/* Before optimization */
.button {
margin-top: 10px;
margin-right: 15px;
margin-bottom: 10px;
margin-left: 15px;
padding-top: 8px;
padding-right: 16px;
padding-bottom: 8px;
padding-left: 16px;
background-color: #007acc;
border-top-left-radius: 4px;
border-top-right-radius: 4px;
border-bottom-left-radius: 4px;
border-bottom-right-radius: 4px;
}
/* After optimization (shorthand) */
.button {
margin: 10px 15px;
padding: 8px 16px;
background: #007acc;
border-radius: 4px;
}
/* Remove redundant rules */
/* ❌ BAD: Duplicate properties */
.box {
color: red;
width: 100px;
color: blue; /* Overrides red, remove red */
}
/* ✅ GOOD */
.box {
width: 100px;
color: blue;
}
/* Combine selectors */
/* ❌ BAD: Repeated rules */
.header { margin: 0; padding: 1rem; }
.footer { margin: 0; padding: 1rem; }
.sidebar { margin: 0; padding: 1rem; }
/* ✅ GOOD: Combined */
.header, .footer, .sidebar {
margin: 0;
padding: 1rem;
}
/* Remove vendor prefixes (use autoprefixer) */
/* ❌ BAD: Manual prefixes */
.box {
-webkit-transform: rotate(45deg);
-moz-transform: rotate(45deg);
-ms-transform: rotate(45deg);
-o-transform: rotate(45deg);
transform: rotate(45deg);
}
/* ✅ GOOD: Let autoprefixer handle it */
.box {
transform: rotate(45deg);
}
/* Use shorter color values */
/* ❌ LONGER */
.box {
color: #ff0000;
background: rgba(255, 255, 255, 1);
}
/* ✅ SHORTER */
.box {
color: red; /* or #f00 */
background: #fff;
}
/* Remove unnecessary units */
/* ❌ UNNECESSARY */
.box {
margin: 0px;
padding: 0em;
line-height: 1.5rem;
}
/* ✅ CLEANER */
.box {
margin: 0;
padding: 0;
line-height: 1.5;
}
/* Measurement targets:
- Critical CSS: < 14KB (uncompressed)
- Total CSS: < 100KB (uncompressed)
- Gzipped: < 30KB
- Number of rules: < 1000
- Selector depth: < 3 levels
*/
CSS Performance Best Practices
- Keep selectors simple (1-3 levels), prefer classes over complex nesting
- Use
contain: layout painton independent components for layout isolation - Apply
content-visibility: autoto off-screen sections (huge performance gain) - Use
will-changeonly for transform/opacity, add before animation, remove after - Inline critical CSS (<14KB), preload important resources, async load rest
- Batch DOM reads then writes to avoid layout thrashing
- Use
transformandopacityfor animations (GPU-accelerated, no reflow) - Remove unused CSS with PurgeCSS (50-90% size reduction)
- Enable Brotli/Gzip compression (70-80% reduction)
- Split CSS by route, lazy load component styles on demand