PatternsCode Splitting Strategies
CSS Splitting
Extract and inline critical CSS, lazy load the rest
CSS Splitting
CSS can be just as heavy as JavaScript. Extract critical CSS, inline it, and lazy load the rest for optimal First Contentful Paint (FCP).
🎯 The Problem
Without CSS Splitting:
main.css: 500 KB
└─ Blocks render until fully downloaded
└─ FCP: 3.2s ❌
With CSS Splitting:
critical.css: 15 KB (inlined in HTML)
├─ Instant render
├─ FCP: 0.6s ✅
route-dashboard.css: 50 KB (lazy loaded)
route-settings.css: 30 KB (lazy loaded)
vendors.css: 200 KB (lazy loaded)📊 Impact on Core Web Vitals
| Metric | Without CSS Splitting | With CSS Splitting |
|---|---|---|
| FCP | 3.2s | 0.6s (81% faster) |
| LCP | 4.1s | 1.2s (71% faster) |
| CLS | 0.25 | 0.05 (80% better) |
🔧 Critical CSS Extraction
Using Critters (Recommended)
npm install --save-dev critters// next.config.js
const CrittersWebpackPlugin = require('critters-webpack-plugin');
module.exports = {
webpack: (config, { isServer }) => {
if (!isServer) {
config.plugins.push(
new CrittersWebpackPlugin({
preload: 'swap', // Preload remaining CSS
pruneSource: true, // Remove inlined CSS from external files
})
);
}
return config;
}
};Manual Critical CSS
// lib/critical-css.ts
export function extractCriticalCSS(html: string): string {
// Extract CSS for above-the-fold content
const criticalSelectors = [
'header',
'nav',
'.hero',
'.above-fold',
'.main-content'
];
// Generate CSS for critical selectors only
return criticalSelectors
.map(selector => extractStylesForSelector(selector))
.join('\n');
}// pages/_document.tsx (Next.js)
import Document, { Html, Head, Main, NextScript } from 'next/document';
class MyDocument extends Document {
render() {
const criticalCSS = extractCriticalCSS(this.props.__NEXT_DATA__.page);
return (
<Html>
<Head>
{/* Inline critical CSS */}
<style dangerouslySetInnerHTML={{ __html: criticalCSS }} />
{/* Preload full CSS (non-blocking) */}
<link
rel="preload"
href="/styles/main.css"
as="style"
onload="this.onload=null;this.rel='stylesheet'"
/>
<noscript>
<link rel="stylesheet" href="/styles/main.css" />
</noscript>
</Head>
<body>
<Main />
<NextScript />
</body>
</Html>
);
}
}📦 CSS Code Splitting by Route
Vite (Automatic)
// vite.config.js
export default {
build: {
cssCodeSplit: true, // Split CSS by routes automatically
}
};
// Results in:
// index.css (home page)
// dashboard.css (dashboard page)
// settings.css (settings page)Webpack
// webpack.config.js
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
module.exports = {
plugins: [
new MiniCssExtractPlugin({
filename: '[name].[contenthash].css',
chunkFilename: '[id].[contenthash].css',
})
],
module: {
rules: [
{
test: /\.css$/,
use: [
MiniCssExtractPlugin.loader,
'css-loader'
]
}
]
}
};🎨 CSS-in-JS Code Splitting
Styled Components
import { lazy, Suspense } from 'react';
// Heavy component with lots of styles
const Dashboard = lazy(() => import('./Dashboard'));
function App() {
return (
<Suspense fallback={<Spinner />}>
<Dashboard />
</Suspense>
);
}
// Dashboard.tsx
import styled from 'styled-components';
// Styles only load when Dashboard loads!
const Container = styled.div`
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 2rem;
// ... 100+ lines of styles
`;
export default function Dashboard() {
return <Container>...</Container>;
}Emotion
/** @jsxImportSource @emotion/react */
import { css } from '@emotion/react';
import { lazy } from 'react';
const HeavyComponent = lazy(() => import('./HeavyComponent'));
// Styles in HeavyComponent only load when component loads🚀 Lazy Load CSS
Dynamic CSS Import
function Dashboard() {
useEffect(() => {
// Lazy load dashboard-specific CSS
import('./Dashboard.css');
}, []);
return <div className="dashboard">...</div>;
}Load CSS on Interaction
function BlogPost() {
const [showComments, setShowComments] = useState(false);
const handleShowComments = () => {
// Load comments CSS only when showing comments
import('./Comments.css').then(() => {
setShowComments(true);
});
};
return (
<div>
<article>Blog content</article>
<button onClick={handleShowComments}>
Show Comments
</button>
{showComments && <Comments />}
</div>
);
}📐 CSS Modules (Automatic Splitting)
// Dashboard.module.css
.container {
display: grid;
/* ... */
}
// Dashboard.tsx
import styles from './Dashboard.module.css';
function Dashboard() {
return <div className={styles.container}>...</div>;
}
// CSS only loads when Dashboard component loads!🎯 Media Query Splitting
Load CSS Based on Screen Size
function App() {
const isMobile = useMediaQuery('(max-width: 768px)');
useEffect(() => {
if (isMobile) {
import('./styles/mobile.css');
} else {
import('./styles/desktop.css');
}
}, [isMobile]);
return <div>...</div>;
}Load Print CSS on Demand
function PrintButton() {
const handlePrint = () => {
// Load print styles only when printing
import('./print.css').then(() => {
window.print();
});
};
return <button onClick={handlePrint}>Print</button>;
}🎨 Tailwind CSS Optimization
Remove Unused Classes
// tailwind.config.js
module.exports = {
content: [
'./pages/**/*.{js,ts,jsx,tsx}',
'./components/**/*.{js,ts,jsx,tsx}',
],
// PurgeCSS removes unused classes automatically
};
// Before: 3 MB of Tailwind
// After: 15 KB (only classes you use)Critical Tailwind
<!-- Inline critical Tailwind classes -->
<style>
/* Only utility classes used above the fold */
.flex { display: flex; }
.grid { display: grid; }
.p-4 { padding: 1rem; }
.text-lg { font-size: 1.125rem; }
/* ... */
</style>
<!-- Load full Tailwind later -->
<link rel="preload" href="/styles/tailwind.css" as="style"
onload="this.rel='stylesheet'">🔧 Font Loading Strategy
Inline Critical Fonts
<head>
<!-- Inline critical font (woff2, base64) -->
<style>
@font-face {
font-family: 'Inter';
font-style: normal;
font-weight: 400;
font-display: swap;
src: url(data:font/woff2;base64,...) format('woff2');
unicode-range: U+0000-00FF; /* Latin only */
}
</style>
<!-- Load full font set later -->
<link rel="preload" href="/fonts/inter-full.woff2" as="font"
type="font/woff2" crossorigin>
</head>Subset Fonts
# Use only characters you need
npx glyphhanger --subset=*.woff2 --formats=woff2 \
--whitelist="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"
# Before: 500 KB
# After: 50 KB (90% smaller!)📊 Measuring CSS Performance
// Measure CSS load time
const observer = new PerformanceObserver((list) => {
for (const entry of list.getEntries()) {
if (entry.initiatorType === 'css' || entry.initiatorType === 'link') {
console.log(`CSS loaded: ${entry.name} in ${entry.duration}ms`);
// Send to analytics
analytics.track('css_loaded', {
file: entry.name,
duration: entry.duration,
size: entry.transferSize
});
}
}
});
observer.observe({ entryTypes: ['resource'] });🎯 Decision Tree
Is CSS > 50 KB?
├─ Yes → Split it
└─ No → Keep inline
Is it critical (above fold)?
├─ Yes → Inline it
└─ No → Lazy load
Is it route-specific?
├─ Yes → Split by route
└─ No → Load globally
Is it component-specific?
├─ Yes → CSS Modules / CSS-in-JS
└─ No → Global stylesheet📚 Best Practices
1. Inline Critical CSS (< 15 KB)
<!-- Above fold only -->
<style>
header { /* ... */ }
.hero { /* ... */ }
nav { /* ... */ }
</style>2. Preload Non-Critical
<link rel="preload" href="/styles/main.css" as="style"
onload="this.onload=null;this.rel='stylesheet'">3. Lazy Load Route CSS
// Each route loads its own CSS
const Dashboard = lazy(() => import('./Dashboard')); // Includes Dashboard.css4. Remove Unused CSS
# Use PurgeCSS
npx purgecss --css main.css --content index.html --output dist/🏢 Real-World Examples
<!-- Inline critical CSS (5 KB) -->
<style>/* above-fold styles */</style>
<!-- Async load rest -->
<link rel="stylesheet" href="/main.css" media="print"
onload="this.media='all'">GitHub
<!-- Split by route -->
<link rel="stylesheet" href="/css/frameworks.css"> <!-- 50 KB -->
<link rel="stylesheet" href="/css/profile.css"> <!-- 20 KB, route-specific -->📚 Key Takeaways
- Inline critical CSS - < 15 KB, above the fold only
- Split by route - Each route gets its own CSS
- Lazy load non-critical - Below fold, interactions
- Remove unused - PurgeCSS, tree-shaking
- CSS Modules - Automatic splitting per component
- Preload fonts - Critical fonts only
- Monitor size - Target < 50 KB per chunk
CSS splitting typically improves FCP by 70-80%. Inline critical, lazy load the rest! 🚀