CSS Performance — Critical CSS, Minification, and Optimization
In this tutorial, you will learn how to optimize CSS performance by extracting critical inline styles, minifying files, removing unused CSS, and writing efficient selectors. CSS blocks rendering and contributes to page weight, making optimization essential for fast first paint. DodaTech reduced CSS-related blocking time by 85 percent through systematic optimization.
What You Will Learn
- How to extract and inline critical above-the-fold CSS
- How to minify CSS and remove unused rules with PurgeCSS
- How to write efficient CSS selectors that the browser can evaluate quickly
- How to use CSS containment and content-visibility for rendering optimization
Why It Matters
CSS is a render-blocking resource by default. A 100KB CSS file can delay first paint by 500ms or more on slow connections. Additionally, inefficient selectors and unused CSS waste CPU time during style recalculation. DodaTech cut its CSS payload from 180KB to 22KB (gzipped) through extraction and purging.
Real-World Use Case
The DodaZIP web interface used Bootstrap 5 with a custom theme, resulting in 220KB of CSS. Only 28KB was needed for the initial viewport. After extracting critical CSS and purging unused rules, the blocking CSS dropped to 15KB and the remaining styles loaded asynchronously. First Contentful Paint improved from 2.4 seconds to 0.9 seconds.
Prerequisites
You should understand CSS selectors and the Critical Rendering Path. Familiarity with HTML document structure is required.
Step-by-Step Tutorial
Step 1: Measure CSS Blocking Impact
Use Lighthouse and Chrome DevTools to determine how much CSS blocks rendering.
# Lighthouse CSS blocking report
npx lighthouse https://example.com --output json --quiet \
| python3 -c "
import sys, json
d = json.load(sys.stdin)
items = d['audits']['render-blocking-resources']['details']['items']
for i in items:
if 'css' in i['url']:
print(f\"{i['url']}: {i['wastedMs']}ms\")
"
Expected output: Lists external CSS files and their estimated blocking time. A typical Bootstrap file may show 300-500ms wasted.
Step 2: Extract Critical CSS
Critical CSS contains only the styles needed for above-the-fold content. Use the Critical tool to extract it automatically.
# Install and run Critical
npm install -g critical
critical https://dodatech.com --base . --width 375 --height 812 > critical.css
Expected output: A critical.css file of approximately 3-8KB containing only the styles visible in the 375x812 viewport.
// Programmatic critical CSS extraction
const critical = require('critical').stream;
const gulp = require('gulp');
gulp.task('critical', () => {
return gulp.src('dist/**/*.html')
.pipe(critical({
inline: true,
css: ['dist/styles/main.css'],
dimensions: [
{width: 375, height: 812}, // Mobile
{width: 1280, height: 800} // Desktop
]
}))
.pipe(gulp.dest('dist'));
});
Step 3: Remove Unused CSS with PurgeCSS
PurgeCSS removes CSS rules that are not used in your HTML or JavaScript.
// PurgeCSS configuration
const PurgeCSS = require('purgecss').PurgeCSS;
const result = await new PurgeCSS().purge({
content: ['dist/**/*.html', 'dist/**/*.js'],
css: ['dist/styles/main.css'],
safelist: {
standard: [/^hljs/], // Keep syntax highlighting classes
deep: [/modal$/]
}
});
result.forEach(out => {
require('fs').writeFileSync(out.file, out.css);
});
Expected output: The CSS file size drops significantly. For Bootstrap projects, typically from 180KB to 20-30KB. Compare file sizes before and after:
ls -lh dist/styles/main.css
# Before: 180KB
# After: 24KB
Step 4: Minify CSS
Minification removes whitespace, comments, and shortens property names where possible.
// Using cssnano for minification
const postcss = require('postcss');
const cssnano = require('cssnano');
const minified = await postcss([cssnano]).process(css, {from: 'main.css'});
console.log(`Original: ${css.length} bytes, Minified: ${minified.css.length} bytes`);
Expected reduction: CSS files typically compress to 60-70 percent of their original size through minification.
Step 5: Write Efficient Selectors
The browser reads CSS selectors from right to left. More specific selectors are evaluated faster.
/* Slow: descendant selector on tag */
body main section article p a { color: red; }
/* Fast: class-based selector */
.text-link { color: red; }
/* Slow: universal selector in combination */
.container > * { margin: 0; }
/* Fast: explicit class */
.container > .row { margin: 0; }
Selector efficiency ranking (fastest to slowest):
- ID selector (
#id) - Class selector (
.class) - Tag selector (
div) - Adjacent sibling (
div + p) - Child selector (
div > p) - Descendant selector (
div p) - Universal selector (
*)
Step 6: Use CSS Containment
CSS containment tells the browser that a subtree does not affect the rest of the page, allowing layout optimizations.
/* Isolate a widget from the rest of the page layout */
.widget {
contain: layout style paint;
}
/* For known fixed-size elements */
.fixed-sidebar {
contain: size layout;
width: 300px;
height: 100vh;
}
Step 7: Use content-visibility for Offscreen Content
The content-visibility property skips rendering of offscreen elements until they scroll into view.
/* Lazy render offscreen sections */
.blog-post {
content-visibility: auto;
contain-intrinsic-size: 500px; /* Reserve space to prevent scrollbar jank */
}
/* The first 3 posts render immediately */
.blog-post:nth-child(-n+3) {
content-visibility: visible;
}
Expected behavior: On a page with 50 blog posts, only the first 3 render initially. As the user scrolls, each post renders just before entering the viewport. Initial rendering time drops significantly.
Step 8: Load Non-Critical CSS Async
Load CSS that is not needed for the initial render asynchronously.
<!-- Before: blocking CSS -->
<link rel="stylesheet" href="styles.css">
<!-- After: critical CSS inlined, non-critical loaded async -->
<style>
/* Critical CSS inlined here */
</style>
<link rel="preload" href="styles.css" as="style"
onload="this.onload=null;this.rel='stylesheet'">
<noscript>
<link rel="stylesheet" href="styles.css">
</noscript>
Learning Path
flowchart LR A[Bundle Optimization] --> B[CSS Performance] B --> C[Critical Rendering Path] B --> D[Render-Blocking Resources] C --> E[Performance Budgets] style B fill:#4f46e5,color:#fff style A fill:#6366f1,color:#fff style C fill:#6366f1,color:#fff
Common Errors
Not using a Css Preprocessor or utility framework: Writing raw CSS without organization leads to selector bloat. Use Sass or Tailwind CSS with PurgeCSS to keep output small.
Over-qualified selectors:
div.container ul.list li.itemis overly specific. Use.container .list-itemor just.list-item.Inlining ALL CSS instead of only critical CSS: Inlining the full 180KB stylesheet into the HTML makes the HTML itself slow to download. Only inline critical above-the-fold CSS.
Using !important excessively: !important increases specificity wars and makes styles harder to override, leading to more CSS being written than necessary.
Not removing unused CSS from frameworks: Bootstrap and other frameworks ship CSS for components you may not use. PurgeCSS removes them, but only if configured correctly.
Animating layout-triggering properties: Animating
width,height,margin, ortoptriggers expensive layout recalculations. Usetransformandopacityinstead.
Practice Questions
- Why does CSS block rendering and how do you avoid that?
- What is the difference between critical CSS extraction and CSS minification?
- Which CSS selectors are fastest for the browser to evaluate?
- How does content-visibility improve rendering performance?
- What is CSS containment and when should you use it?
Answers: 1. CSS blocks rendering because the browser needs the CSSOM to construct the render tree. Avoid this by inlining critical CSS and loading the rest asynchronously. 2. Critical CSS extraction removes rules not visible above the fold; minification removes whitespace and comments to reduce file size. 3. ID selectors, followed by class selectors, then tag selectors. Universal and descendant selectors are slowest. 4. It skips rendering and painting of offscreen elements until they scroll into view, reducing initial rendering work. 5. CSS containment isolates a subtree from the rest of the page, allowing the browser to optimize layout and paint for that subtree independently.
Challenge
Analyze a production site CSS. Measure the total CSS size, the amount of critical CSS, and the percentage of unused rules. Extract the critical CSS, purge unused rules with PurgeCSS, minify the result, and load the remaining CSS asynchronously. Measure the before and after FCP using Lighthouse.
FAQ
Built by the developers of DodaTech
Doda Browser, DodaZIP & Durga Antivirus Pro