Skip to content

CSS Performance — Critical CSS, Minification, and Optimization

DodaTech Updated 2026-06-23 7 min read

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):

  1. ID selector (#id)
  2. Class selector (.class)
  3. Tag selector (div)
  4. Adjacent sibling (div + p)
  5. Child selector (div > p)
  6. Descendant selector (div p)
  7. 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

  1. 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.

  2. Over-qualified selectors: div.container ul.list li.item is overly specific. Use .container .list-item or just .list-item.

  3. 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.

  4. Using !important excessively: !important increases specificity wars and makes styles harder to override, leading to more CSS being written than necessary.

  5. 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.

  6. Animating layout-triggering properties: Animating width, height, margin, or top triggers expensive layout recalculations. Use transform and opacity instead.

Practice Questions

  1. Why does CSS block rendering and how do you avoid that?
  2. What is the difference between critical CSS extraction and CSS minification?
  3. Which CSS selectors are fastest for the browser to evaluate?
  4. How does content-visibility improve rendering performance?
  5. 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

Does CSS affect Cumulative Layout Shift (CLS)?

Yes, late-loading CSS can cause elements to shift when styles are applied. Inlining critical CSS and using size attributes prevents CLS from CSS loading.

Should I use Tailwind CSS for performance?

Tailwind CSS generates many small utility classes. When combined with PurgeCSS, the production CSS is typically very small (10-20KB). Without purging, it is large.

How do CSS-in-JS solutions affect performance?

CSS-in-JS libraries inject styles at runtime, which adds JavaScript execution time and can delay rendering. For critical CSS, inline styles or static extraction is faster.

What is the ideal critical CSS size?

Keep critical CSS under 14KB (the size of one TCP packet). This ensures the critical styles arrive in the first round trip.

What tools does DodaTech use for CSS optimization?

DodaTech uses PurgeCSS for unused CSS removal, cssnano for minification, and the Critical npm package for critical CSS extraction. All are integrated into the build pipeline via Vite and PostCSS.

Built by the developers of DodaTech

Doda Browser, DodaZIP & Durga Antivirus Pro