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 paint on independent components for layout isolation
  • Apply content-visibility: auto to off-screen sections (huge performance gain)
  • Use will-change only 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 transform and opacity for 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