Security & Performance
CSS Performance Optimization
CSS Performance Optimization
CSS impacts both load time and runtime performance. Poorly optimized CSS can cause slow page renders, janky animations, and unnecessary repaints. This lesson covers techniques to optimize CSS delivery, reduce rendering costs, and improve visual performance through modern CSS features and best practices.
Critical CSS
Inline critical above-the-fold CSS to eliminate render-blocking:
<!-- Inline critical CSS in <head> -->
<style>
/* Only styles for above-the-fold content */
.header { background: #333; padding: 1rem; }
.hero { min-height: 100vh; display: flex; }
.hero h1 { font-size: 3rem; color: #fff; }
</style>
<!-- Load remaining CSS asynchronously -->
<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>
<style>
/* Only styles for above-the-fold content */
.header { background: #333; padding: 1rem; }
.hero { min-height: 100vh; display: flex; }
.hero h1 { font-size: 3rem; color: #fff; }
</style>
<!-- Load remaining CSS asynchronously -->
<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>
Note: Critical CSS should be under 14 KB (the size of the first TCP packet) to maximize first paint performance. Tools like Critical or PurgeCSS can automate extraction.
CSS Containment
Use CSS containment to limit layout and paint scope:
/* Isolate component rendering */
.card {
contain: layout style paint;
/* Browser knows changes inside won't affect outside */
}
/* Strict containment for completely isolated widgets */
.widget {
contain: strict;
/* Equivalent to: layout style paint size */
}
/* Content containment for dynamic content */
.content-area {
contain: content;
/* Equivalent to: layout style paint */
}
/* Example: Optimizing a list */
.list-item {
contain: layout style;
/* Changes to one item don't trigger reflow of others */
}
.card {
contain: layout style paint;
/* Browser knows changes inside won't affect outside */
}
/* Strict containment for completely isolated widgets */
.widget {
contain: strict;
/* Equivalent to: layout style paint size */
}
/* Content containment for dynamic content */
.content-area {
contain: content;
/* Equivalent to: layout style paint */
}
/* Example: Optimizing a list */
.list-item {
contain: layout style;
/* Changes to one item don't trigger reflow of others */
}
Tip: Use `content-visibility: auto` for off-screen content to skip rendering entirely. This can dramatically improve initial render time for long pages.
content-visibility Property
/* Skip rendering off-screen content */
.section {
content-visibility: auto;
contain-intrinsic-size: 0 500px; /* Reserve space */
}
/* Example: Long article with sections */
<style>
.article-section {
content-visibility: auto;
contain-intrinsic-size: 0 400px;
}
</style>
<article>
<section class="article-section">...</section>
<section class="article-section">...</section>
<section class="article-section">...</section>
</article>
/* Result: Only visible sections are rendered */
/* Can reduce rendering time by 50-70% on long pages */
.section {
content-visibility: auto;
contain-intrinsic-size: 0 500px; /* Reserve space */
}
/* Example: Long article with sections */
<style>
.article-section {
content-visibility: auto;
contain-intrinsic-size: 0 400px;
}
</style>
<article>
<section class="article-section">...</section>
<section class="article-section">...</section>
<section class="article-section">...</section>
</article>
/* Result: Only visible sections are rendered */
/* Can reduce rendering time by 50-70% on long pages */
will-change Property
Hint to browser about upcoming changes for optimization:
/* Optimize upcoming animations */
.modal {
will-change: transform, opacity;
}
/* Apply before the change, remove after */
.element:hover {
will-change: transform;
}
.element:active {
transform: scale(1.1);
will-change: auto; /* Reset after animation */
}
/* JavaScript example */
const element = document.querySelector('.animate');
element.addEventListener('mouseenter', () => {
element.style.willChange = 'transform';
});
element.addEventListener('animationend', () => {
element.style.willChange = 'auto';
});
.modal {
will-change: transform, opacity;
}
/* Apply before the change, remove after */
.element:hover {
will-change: transform;
}
.element:active {
transform: scale(1.1);
will-change: auto; /* Reset after animation */
}
/* JavaScript example */
const element = document.querySelector('.animate');
element.addEventListener('mouseenter', () => {
element.style.willChange = 'transform';
});
element.addEventListener('animationend', () => {
element.style.willChange = 'auto';
});
Warning: Don't overuse `will-change`. It consumes memory and GPU resources. Only use it for elements that will actually change, and remove it when done.
GPU Acceleration
Use transform and opacity for smooth animations:
/* Bad: Triggers layout recalculation */
.box {
transition: width 0.3s, height 0.3s, top 0.3s, left 0.3s;
}
.box:hover {
width: 200px;
height: 200px;
top: 50px;
left: 50px;
}
/* Good: Uses GPU compositing */
.box {
transition: transform 0.3s, opacity 0.3s;
}
.box:hover {
transform: scale(1.5) translate(25px, 25px);
opacity: 0.9;
}
/* Force GPU acceleration with translateZ */
.animated {
transform: translateZ(0); /* Creates new compositing layer */
backface-visibility: hidden; /* Prevents flickering */
}
.box {
transition: width 0.3s, height 0.3s, top 0.3s, left 0.3s;
}
.box:hover {
width: 200px;
height: 200px;
top: 50px;
left: 50px;
}
/* Good: Uses GPU compositing */
.box {
transition: transform 0.3s, opacity 0.3s;
}
.box:hover {
transform: scale(1.5) translate(25px, 25px);
opacity: 0.9;
}
/* Force GPU acceleration with translateZ */
.animated {
transform: translateZ(0); /* Creates new compositing layer */
backface-visibility: hidden; /* Prevents flickering */
}
Reducing Repaints and Reflows
Minimize layout thrashing and unnecessary recalculations:
// Bad: Multiple reflows
const boxes = document.querySelectorAll('.box');
boxes.forEach(box => {
const width = box.offsetWidth; // Read (triggers reflow)
box.style.width = width + 10 + 'px'; // Write (triggers reflow)
});
// Good: Batch reads and writes
const boxes = document.querySelectorAll('.box');
const widths = Array.from(boxes).map(box => box.offsetWidth);
boxes.forEach((box, i) => {
box.style.width = widths[i] + 10 + 'px';
});
// Use CSS classes instead of inline styles
// Bad: Forces style recalculation
element.style.width = '100px';
element.style.height = '100px';
element.style.background = 'red';
// Good: Single class change
element.classList.add('large-red-box');
// Use DocumentFragment for multiple DOM insertions
const fragment = document.createDocumentFragment();
for (let i = 0; i < 100; i++) {
const div = document.createElement('div');
fragment.appendChild(div);
}
container.appendChild(fragment); // Single reflow
const boxes = document.querySelectorAll('.box');
boxes.forEach(box => {
const width = box.offsetWidth; // Read (triggers reflow)
box.style.width = width + 10 + 'px'; // Write (triggers reflow)
});
// Good: Batch reads and writes
const boxes = document.querySelectorAll('.box');
const widths = Array.from(boxes).map(box => box.offsetWidth);
boxes.forEach((box, i) => {
box.style.width = widths[i] + 10 + 'px';
});
// Use CSS classes instead of inline styles
// Bad: Forces style recalculation
element.style.width = '100px';
element.style.height = '100px';
element.style.background = 'red';
// Good: Single class change
element.classList.add('large-red-box');
// Use DocumentFragment for multiple DOM insertions
const fragment = document.createDocumentFragment();
for (let i = 0; i < 100; i++) {
const div = document.createElement('div');
fragment.appendChild(div);
}
container.appendChild(fragment); // Single reflow
Note: Reading layout properties (offsetWidth, offsetHeight, getComputedStyle) forces synchronous layout. Batch all reads before writes to avoid layout thrashing.
CSS-in-JS Performance
Optimize runtime CSS generation:
// Bad: Creates new style object every render
function Component({ color }) {
return (
<div style={{ backgroundColor: color, padding: '20px' }}>
Content
</div>
);
}
// Good: Extract static styles
const staticStyles = { padding: '20px' };
function Component({ color }) {
return (
<div style={{ ...staticStyles, backgroundColor: color }}>
Content
</div>
);
}
// Better: Use CSS classes with dynamic variables
const styles = `
.component {
padding: 20px;
background-color: var(--bg-color);
}
`;
function Component({ color }) {
return (
<div className="component" style={{ '--bg-color': color }}>
Content
</div>
);
}
function Component({ color }) {
return (
<div style={{ backgroundColor: color, padding: '20px' }}>
Content
</div>
);
}
// Good: Extract static styles
const staticStyles = { padding: '20px' };
function Component({ color }) {
return (
<div style={{ ...staticStyles, backgroundColor: color }}>
Content
</div>
);
}
// Better: Use CSS classes with dynamic variables
const styles = `
.component {
padding: 20px;
background-color: var(--bg-color);
}
`;
function Component({ color }) {
return (
<div className="component" style={{ '--bg-color': color }}>
Content
</div>
);
}
Purging Unused CSS
Remove unused styles to reduce bundle size:
// PurgeCSS configuration (purge.config.js)
module.exports = {
content: [
'./src/**/*.html',
'./src/**/*.js',
'./src/**/*.jsx',
],
css: ['./src/styles/**/*.css'],
output: './dist/styles/',
safelist: [
'active', 'open', 'visible', // Dynamic classes
/^modal-/, // Regex patterns
]
};
// Tailwind CSS purge configuration
module.exports = {
purge: {
enabled: true,
content: ['./src/**/*.{html,js,jsx,ts,tsx}'],
options: {
safelist: ['bg-red-500', 'text-center'],
}
},
};
// Result: Can reduce CSS from 500 KB to 10-20 KB
module.exports = {
content: [
'./src/**/*.html',
'./src/**/*.js',
'./src/**/*.jsx',
],
css: ['./src/styles/**/*.css'],
output: './dist/styles/',
safelist: [
'active', 'open', 'visible', // Dynamic classes
/^modal-/, // Regex patterns
]
};
// Tailwind CSS purge configuration
module.exports = {
purge: {
enabled: true,
content: ['./src/**/*.{html,js,jsx,ts,tsx}'],
options: {
safelist: ['bg-red-500', 'text-center'],
}
},
};
// Result: Can reduce CSS from 500 KB to 10-20 KB
Tip: Use PostCSS with cssnano to minify and optimize CSS. Enable features like merging rules, removing duplicates, and optimizing calc() expressions.
Optimizing Selectors
/* Bad: Complex selectors */
body div.container ul li a.link { color: blue; }
/* Browser reads right-to-left, checks every anchor */
/* Good: Simple, specific selectors */
.nav-link { color: blue; }
/* Bad: Universal selector */
* { box-sizing: border-box; }
/* Good: Target specific elements */
html { box-sizing: border-box; }
*, *::before, *::after { box-sizing: inherit; }
/* Use BEM for flat specificity */
.card { }
.card__header { }
.card__title { }
.card--featured { }
/* Avoid expensive pseudo-selectors */
/* Bad */
:nth-child(n+1):nth-child(-n+10) { }
/* Good */
.item:nth-child(-n+10) { }
body div.container ul li a.link { color: blue; }
/* Browser reads right-to-left, checks every anchor */
/* Good: Simple, specific selectors */
.nav-link { color: blue; }
/* Bad: Universal selector */
* { box-sizing: border-box; }
/* Good: Target specific elements */
html { box-sizing: border-box; }
*, *::before, *::after { box-sizing: inherit; }
/* Use BEM for flat specificity */
.card { }
.card__header { }
.card__title { }
.card--featured { }
/* Avoid expensive pseudo-selectors */
/* Bad */
:nth-child(n+1):nth-child(-n+10) { }
/* Good */
.item:nth-child(-n+10) { }
CSS Loading Strategies
<!-- Strategy 1: Critical + Async -->
<style>/* Critical CSS */</style>
<link rel="preload" href="main.css" as="style" onload="this.rel='stylesheet'">
<!-- Strategy 2: Media queries for conditional loading -->
<link rel="stylesheet" href="print.css" media="print">
<link rel="stylesheet" href="mobile.css" media="(max-width: 768px)">
<!-- Strategy 3: Preload for high-priority resources -->
<link rel="preload" href="fonts/main.woff2" as="font" crossorigin>
<link rel="preload" href="critical.css" as="style">
<!-- Strategy 4: DNS prefetch for external CSS -->
<link rel="dns-prefetch" href="//fonts.googleapis.com">
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Inter">
<style>/* Critical CSS */</style>
<link rel="preload" href="main.css" as="style" onload="this.rel='stylesheet'">
<!-- Strategy 2: Media queries for conditional loading -->
<link rel="stylesheet" href="print.css" media="print">
<link rel="stylesheet" href="mobile.css" media="(max-width: 768px)">
<!-- Strategy 3: Preload for high-priority resources -->
<link rel="preload" href="fonts/main.woff2" as="font" crossorigin>
<link rel="preload" href="critical.css" as="style">
<!-- Strategy 4: DNS prefetch for external CSS -->
<link rel="dns-prefetch" href="//fonts.googleapis.com">
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Inter">
Exercise: Audit your site's CSS using Chrome DevTools Coverage tab. Identify unused CSS (typically 60-80% in frameworks). Configure PurgeCSS or similar tool to remove unused styles. Extract critical CSS for above-the-fold content and implement async loading. Measure the impact on First Contentful Paint (FCP) using Lighthouse before and after optimization.