/* 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 animationelement.style.willChange = 'transform';// Start animationelement.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); }}// Usageconst optimizer = new AnimationOptimizer(document.querySelector('.card'));// Before hover animationcard.addEventListener('mouseenter', () => { optimizer.optimize('transform');});card.addEventListener('mouseleave', () => { optimizer.deoptimize();});// For FLIP animationsfunction 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 impactconsole.memory.usedJSHeapSize // Beforeelement.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>
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 readsconst heights = Array.from(elements).map(el => el.offsetHeight);// Then: Batch all writeselements.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.offsetLeftel.clientHeight, el.clientWidth, el.clientTop, el.clientLeftel.scrollHeight, el.scrollWidth, el.scrollTop, el.scrollLeftel.getBoundingClientRect()window.getComputedStyle()// Setting these properties causes reflow:width, height, margin, padding, bordertop, right, bottom, leftfont-size, line-heightdisplay, position, float*//* Properties that only repaint (MODERATE): *//*color, background, background-colorborder-color, box-shadowvisibility, 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 100container.appendChild(fragment);*//* Use classList for multiple class changes *//* ❌ BAD: Multiple reflows *//*el.className = 'box'; // Reflowel.className = 'box active'; // Reflowel.className = 'box active visible'; // Reflow*//* ✅ GOOD: Single update *//*el.className = 'box active visible'; // One reflow// Or use classListel.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'; // Reflowel.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 = [];}// UsagescheduleUpdate(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.