Critical CSS and Performance Optimization
Answer
Critical CSS is the minimum CSS required to render above-the-fold content. Extracting and inlining it improves First Contentful Paint (FCP) by eliminating render-blocking stylesheets.
The Problem
The Solution
Implementing Critical CSS
<!DOCTYPE html>
<html>
<head>
<!-- Critical CSS inlined -->
<style>
/* Only styles needed for above-the-fold content */
body {
margin: 0;
font-family: system-ui, sans-serif;
}
.header {
height: 60px;
background: #333;
color: white;
display: flex;
align-items: center;
padding: 0 20px;
}
.hero {
min-height: 400px;
background: linear-gradient(135deg, #667eea, #764ba2);
display: flex;
align-items: center;
justify-content: center;
}
.hero h1 {
color: white;
font-size: 3rem;
}
</style>
<!-- Full CSS loaded asynchronously -->
<link
rel="preload"
href="styles.css"
as="style"
onload="this.onload=null;this.rel='stylesheet'"
/>
<noscript><link rel="stylesheet" href="styles.css" /></noscript>
</head>
<body>
<!-- Above-the-fold content -->
<header class="header">Logo</header>
<section class="hero">
<h1>Welcome</h1>
</section>
<!-- Below-the-fold content -->
<!-- ... -->
</body>
</html>
Automated Critical CSS Extraction
// Using Critical (Node.js)
const critical = require("critical");
critical.generate({
base: "dist/",
src: "index.html",
target: {
html: "index-critical.html",
css: "critical.css",
},
width: 1300,
height: 900,
inline: true,
});
// Webpack with critters
// webpack.config.js
const Critters = require("critters-webpack-plugin");
module.exports = {
plugins: [
new Critters({
preload: "swap",
pruneSource: true,
}),
],
};
CSS Loading Strategies
<!-- 1. Preload + async load -->
<link
rel="preload"
href="styles.css"
as="style"
onload="this.onload=null;this.rel='stylesheet'"
/>
<!-- 2. Media query trick (downloads but doesn't block) -->
<link
rel="stylesheet"
href="styles.css"
media="print"
onload="this.media='all'"
/>
<!-- 3. loadCSS library -->
<script>
!function(e){"use strict";var t=function(t,n,r,o){...}(window)
</script>
<noscript><link rel="stylesheet" href="styles.css" /></noscript>
<!-- 4. Dynamic import -->
<script>
const link = document.createElement("link");
link.rel = "stylesheet";
link.href = "styles.css";
document.head.appendChild(link);
</script>
Code Splitting CSS
// Webpack CSS extraction per entry
module.exports = {
entry: {
main: "./src/index.js",
about: "./src/about.js",
},
plugins: [
new MiniCssExtractPlugin({
filename: "[name].[contenthash].css",
}),
],
};
// Lazy-loaded routes can have their own CSS
// React with code splitting
const About = React.lazy(() => import("./About"));
// About.css only loads when About component is needed
Removing Unused CSS
// PurgeCSS configuration
// purgecss.config.js
module.exports = {
content: ["./src/**/*.html", "./src/**/*.js", "./src/**/*.jsx"],
css: ["./src/**/*.css"],
safelist: [
/^modal-/, // Keep classes matching pattern
"active",
],
output: "./dist/",
};
// With Tailwind CSS (built-in purging)
// tailwind.config.js
module.exports = {
content: [
"./pages/**/*.{js,ts,jsx,tsx}",
"./components/**/*.{js,ts,jsx,tsx}",
],
// Unused utilities are removed in production
};
Measuring CSS Performance
// Performance API
const cssEntries = performance
.getEntriesByType("resource")
.filter(
(entry) => entry.initiatorType === "link" || entry.name.endsWith(".css")
);
cssEntries.forEach((entry) => {
console.log(`${entry.name}:`, {
size: entry.transferSize,
duration: entry.duration,
blocked: entry.renderBlockingStatus,
});
});
// Web Vitals
import { getCLS, getFID, getLCP } from "web-vitals";
getCLS(console.log);
getFID(console.log);
getLCP(console.log);
Performance Checklist
Optimization Summary
| Technique | Impact | Effort |
|---|---|---|
| Critical CSS | High | Medium |
| Remove unused | High | Low |
| Async loading | Medium | Low |
| Minification | Medium | Low |
| Code splitting | Medium | Medium |
| Gzip/Brotli | Medium | Low |
| CDN caching | High | Low |
Tools
- Critical: Critical CSS extraction
- PurgeCSS: Remove unused CSS
- cssnano: Minification
- Critters: Webpack plugin for critical CSS
- Coverage (DevTools): Find unused CSS
- Lighthouse: Performance auditing
Key Points
- Extract and inline above-the-fold CSS
- Load remaining CSS asynchronously
- Remove unused CSS with PurgeCSS
- Use tools like Critical, Critters automatically
- Measure impact with DevTools and Web Vitals
- Critical CSS should be ~14KB or less (ideal)