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 --minify

JavaScript 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=300

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