HomeCSS Tailwind
Chapter 9
09 — Performance Optimization
Critical Rendering Path
1. HTML → DOM
2. CSS → CSSOM
3. DOM + CSSOM → Render Tree
4. Layout (calculate positions)
5. Paint (fill pixels)
6. Composite (layers together)Goal: Minimize critical resources, bytes, and path length.
CSS Optimization
Eliminate Render-Blocking
<!-- Inline critical CSS -->
<style>
/* Above-the-fold styles */
body { margin: 0; font-family: system-ui; }
.hero { height: 100vh; background: #333; }
</style>
<!-- Load rest asynchronously -->
<link rel="stylesheet" href="styles.css" media="print" onload="this.media='all'">
<noscript><link rel="stylesheet" href="styles.css"></noscript>Tailwind Purge (Unused CSS)
// tailwind.config.js
module.exports = {
content: [
'./src/**/*.{html,js,ts,jsx,tsx}',
],
// Purge removes unused styles in production
}Minify CSS
# Build command
NODE_ENV=production npx tailwindcss -i ./src/input.css -o ./dist/output.css --minifyJavaScript Optimization
Defer Non-Critical JS
<!-- Defer: Download in parallel, execute after HTML -->
<script src="app.js" defer></script>
<!-- Async: Download and execute asap -->
<script src="analytics.js" async></script>
<!-- Module (defer by default) -->
<script type="module" src="app.js"></script>Code Splitting
// Dynamic import (React)
const LazyComponent = React.lazy(() => import('./LazyComponent'));
function App() {
return (
<React.Suspense fallback={<div>Loading...</div>}>
<LazyComponent />
</React.Suspense>
);
}Tree Shaking
// Named imports (tree-shakeable)
import { debounce } from 'lodash-es';
// Avoid (pulls entire library)
import _ from 'lodash';Image Optimization
Modern Formats
<!-- WebP/AVIF for modern browsers -->
<picture>
<source srcset="image.avif" type="image/avif">
<source srcset="image.webp" type="image/webp">
<img src="image.jpg" alt="Fallback">
</picture>Responsive Images
<!-- Different sizes for different viewports -->
<img
src="small.jpg"
srcset="small.jpg 480w, medium.jpg 800w, large.jpg 1200w"
sizes="(max-width: 600px) 480px, 800px"
alt="Description"
>Lazy Loading
<!-- Native lazy loading -->
<img src="image.jpg" loading="lazy" alt="...">
<!-- Background images -->
<div class="bg-[url('image.jpg')]" loading="lazy"></div>CDN Optimization
<!-- Use image CDN for resizing, compression -->
<img src="https://cdn.example.com/w=800,h=600,q=80/image.jpg">Font Optimization
Preload Critical Fonts
<link rel="preload" href="font.woff2" as="font" type="font/woff2" crossorigin>Font Display
/* Prevent invisible text during load */
@font-face {
font-family: 'Inter';
src: url('inter.woff2') format('woff2');
font-display: swap; /* Show fallback, then swap */
}System Fonts (Fastest)
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI',
Roboto, Oxygen, Ubuntu, sans-serif;
}Caching Strategies
HTTP Caching
# Static assets (1 year)
Cache-Control: public, max-age=31536000, immutable
# HTML (no cache)
Cache-Control: no-cache
# API responses (5 min)
Cache-Control: public, max-age=300Service Workers
// Cache-first strategy
self.addEventListener('fetch', (event) => {
event.respondWith(
caches.match(event.request).then((response) => {
return response || fetch(event.request);
})
);
});Rendering Performance
Avoid Layout Thrashing
// ❌ Bad: Forces reflow each iteration
for (let i = 0; i < 100; i++) {
element.style.width = i + 'px';
console.log(element.offsetWidth); // Forces reflow
}
// ✅ Good: Batch reads and writes
const width = element.offsetWidth; // Read once
for (let i = 0; i < 100; i++) {
element.style.width = i + 'px'; // Write many
}Use transform and opacity
/* ✅ GPU-accelerated, doesn't trigger layout */
.element {
transform: translateX(100px);
opacity: 0.5;
}
/* ❌ Triggers layout, slower */
.element {
margin-left: 100px;
opacity: 0.5;
}Will-Change
/* Hint browser about upcoming changes */
.element {
will-change: transform, opacity;
}Bundle Size
Analyze Dependencies
# Webpack Bundle Analyzer
npx webpack-bundle-analyzer dist/stats.json
# Rollup plugin
import { visualizer } from 'rollup-plugin-visualizer';Remove Unused Code
// lodash-es (tree-shakeable) vs lodash
import debounce from 'lodash-es/debounce'; // ✅
import _ from 'lodash'; // ❌ (entire library)
// Moment.js is heavy, use date-fns or dayjs
import dayjs from 'dayjs'; // ✅ (2kb vs 300kb)Resource Hints
<!-- Preconnect to external domains -->
<link rel="preconnect" href="https://api.example.com">
<!-- DNS prefetch -->
<link rel="dns-prefetch" href="https://api.example.com">
<!-- Prefetch resources for next navigation -->
<link rel="prefetch" href="/next-page.js">
<!-- Preload critical resources -->
<link rel="preload" href="critical.css" as="style">Performance Budget
// package.json
{
"performance": {
"budgets": [{
"type": "bundle",
"name": "main",
"maximumWarning": "200kb"
}]
}
}Monitoring
Core Web Vitals
// LCP: Largest Contentful Paint
new PerformanceObserver((list) => {
const entries = list.getEntries();
console.log('LCP:', entries[entries.length - 1].startTime);
}).observe({ type: 'largest-contentful-paint', buffered: true });
// CLS: Cumulative Layout Shift
new PerformanceObserver((list) => {
let cls = 0;
list.getEntries().forEach(entry => cls += entry.value);
console.log('CLS:', cls);
}).observe({ type: 'layout-shift', buffered: true });Lighthouse
# Run audit
npm run lighthouse
# Target scores
# Performance: 90+
# Accessibility: 90+
# Best Practices: 90+
# SEO: 90+Key Takeaways
- Critical CSS: Inline above-the-fold, async load the rest
- Images: WebP/AVIF, responsive sizes, lazy loading
- Fonts:
font-display: swap, preload critical fonts - JavaScript: Defer, code split, tree shake
- Caching: Long cache for static, no-cache for HTML
- Rendering: Use
transform/opacity, avoid layout thrashing - Bundle: Keep under 200kb gzipped, analyze regularly
- Monitor: Core Web Vitals (LCP, CLS, FID)