CSS blocks page render until fully downloaded and parsed. That's correct behaviour — without it, users would see unstyled HTML flash before CSS arrives. The optimisation is shipping LESS critical CSS so render unblocks sooner. Inline the critical above-the-fold styles, defer everything else. Done properly this lifts First Contentful Paint by 500ms-1.5s on mobile, which Google's Core Web Vitals reward.
Critical CSS is the minimum styles needed for above-the-fold content. Several tools extract it automatically by rendering the page in a headless browser and capturing which CSS rules apply to visible elements.
npm install --save-dev critical
// Build script
const critical = require('critical');
await critical.generate({
src: 'index.html',
target: { html: 'index-critical.html' },
width: 1300,
height: 900,
inline: true,
});
Next.js automatically inlines critical CSS for CSS Modules and styled-jsx. For other CSS, use @next/critical-css or build-step extraction.
Each route's critical CSS differs (homepage hero is different from product page). Extract per route, not site-wide. Tools like Critical accept a list of URLs and generate per-page critical CSS.
<head>
<meta charset="UTF-8">
<title>Page title</title>
<style>
/* Critical CSS — 10-20 KB max */
*{box-sizing:border-box;margin:0;padding:0}
body{font-family:system-ui,sans-serif;line-height:1.5}
.header{background:#1e3a8a;color:#fff;padding:16px 24px}
.hero{padding:64px 24px;text-align:center}
.hero h1{font-size:48px;margin-bottom:16px}
/* ... continued for above-the-fold only ... */
</style>
<!-- Non-critical CSS loaded async -->
<link rel="preload" href="/main.css" as="style" onload="this.onload=null;this.rel='stylesheet'">
<noscript><link rel="stylesheet" href="/main.css"></noscript>
</head>
The inlined styles render immediately. The preload kicks off the full CSS download in parallel. When it arrives, the onload handler converts the preload to a stylesheet and the rest of the page styles apply.
<link rel="preload" href="/main.css" as="style"
onload="this.onload=null;this.rel='stylesheet'">
<noscript><link rel="stylesheet" href="/main.css"></noscript>
<link rel="stylesheet" href="/main.css" media="print"
onload="this.media='all'">
Browser thinks the CSS is for print, so doesn't block render. JavaScript switches media to "all" once loaded.
<script>
const link = document.createElement('link');
link.rel = 'stylesheet';
link.href = '/main.css';
document.head.appendChild(link);
</script>
Works but the JS itself must load first. Less ideal than preload.
Web fonts also block render if loaded synchronously. The @font-face declaration alone doesn't fetch the font — it's only fetched when CSS references it via font-family.
<link rel="preload"
href="/fonts/inter-bold.woff2"
as="font"
type="font/woff2"
crossorigin>
@font-face {
font-family: 'Inter';
src: url('/fonts/inter.woff2') format('woff2');
font-display: swap; /* show fallback while font loads */
}
swap shows the fallback font immediately, then switches when the web font arrives. Better than block (invisible text until font loads) or auto (browser-dependent).
Third-party font services (Google Fonts, Adobe Fonts) add DNS + connection setup time on top of font download. For high-traffic sites, self-hosting cuts 100-300ms.
/* Self-hosted instead of Google Fonts */
@font-face {
font-family: 'Inter';
src: url('/fonts/inter.var.woff2') format('woff2-variations');
font-weight: 100 900;
font-display: swap;
}
Variable fonts (.woff2 with variations) ship one file with all weights, smaller total download.