Performance optimization
Deep performance optimization based on Lighthouse performance audits. Focuses on loading speed, runtime efficiency, and resource optimization.
How it works
-
Identify performance bottlenecks in code and assets
-
Prioritize by impact on Core Web Vitals
-
Provide specific optimizations with code examples
-
Measure improvement with before/after metrics
Performance budget
Resource Budget Rationale
Total page weight < 1.5 MB 3G loads in ~4s
JavaScript (compressed) < 300 KB Parsing + execution time
CSS (compressed) < 100 KB Render blocking
Images (above-fold) < 500 KB LCP impact
Fonts < 100 KB FOIT/FOUT prevention
Third-party < 200 KB Uncontrolled latency
Critical rendering path
Server response
-
TTFB < 800ms. Time to First Byte should be fast. Use CDN, caching, and efficient backends.
-
Enable compression. Gzip or Brotli for text assets. Brotli preferred (15-20% smaller).
-
HTTP/2 or HTTP/3. Multiplexing reduces connection overhead.
-
Edge caching. Cache HTML at CDN edge when possible.
Resource loading
Preconnect to required origins:
<link rel="preconnect" href="https://fonts.googleapis.com"> <link rel="preconnect" href="https://cdn.example.com" crossorigin>
Preload critical resources:
<!-- LCP image --> <link rel="preload" href="/hero.webp" as="image" fetchpriority="high">
<!-- Critical font --> <link rel="preload" href="/font.woff2" as="font" type="font/woff2" crossorigin>
Defer non-critical CSS:
<!-- Critical CSS inlined --> <style>/* Above-fold styles */</style>
<!-- Non-critical CSS --> <link rel="preload" href="/styles.css" as="style" onload="this.onload=null;this.rel='stylesheet'"> <noscript><link rel="stylesheet" href="/styles.css"></noscript>
JavaScript optimization
Defer non-essential scripts:
<!-- Parser-blocking (avoid) --> <script src="/critical.js"></script>
<!-- Deferred (preferred) --> <script defer src="/app.js"></script>
<!-- Async (for independent scripts) --> <script async src="/analytics.js"></script>
<!-- Module (deferred by default) --> <script type="module" src="/app.mjs"></script>
Code splitting patterns:
// Route-based splitting const Dashboard = lazy(() => import('./Dashboard'));
// Component-based splitting const HeavyChart = lazy(() => import('./HeavyChart'));
// Feature-based splitting if (user.isPremium) { const PremiumFeatures = await import('./PremiumFeatures'); }
Tree shaking best practices:
// ❌ Imports entire library import _ from 'lodash'; _.debounce(fn, 300);
// ✅ Imports only what's needed import debounce from 'lodash/debounce'; debounce(fn, 300);
Image optimization
Format selection
Format Use case Browser support
AVIF Photos, best compression 92%+
WebP Photos, good fallback 97%+
PNG Graphics with transparency Universal
SVG Icons, logos, illustrations Universal
Responsive images
<picture> <!-- AVIF for modern browsers --> <source type="image/avif" srcset="hero-400.avif 400w, hero-800.avif 800w, hero-1200.avif 1200w" sizes="(max-width: 600px) 100vw, 50vw">
<!-- WebP fallback --> <source type="image/webp" srcset="hero-400.webp 400w, hero-800.webp 800w, hero-1200.webp 1200w" sizes="(max-width: 600px) 100vw, 50vw">
<!-- JPEG fallback --> <img src="hero-800.jpg" srcset="hero-400.jpg 400w, hero-800.jpg 800w, hero-1200.jpg 1200w" sizes="(max-width: 600px) 100vw, 50vw" width="1200" height="600" alt="Hero image" loading="lazy" decoding="async"> </picture>
LCP image priority
<!-- Above-fold LCP image: eager loading, high priority --> <img src="hero.webp" fetchpriority="high" loading="eager" decoding="sync" alt="Hero">
<!-- Below-fold images: lazy loading --> <img src="product.webp" loading="lazy" decoding="async" alt="Product">
Font optimization
Loading strategy
/* System font stack as fallback */ body { font-family: 'Custom Font', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; }
/* Prevent invisible text / @font-face { font-family: 'Custom Font'; src: url('/fonts/custom.woff2') format('woff2'); font-display: swap; / or optional for non-critical / font-weight: 400; font-style: normal; unicode-range: U+0000-00FF; / Subset to Latin */ }
Preloading critical fonts
<link rel="preload" href="/fonts/heading.woff2" as="font" type="font/woff2" crossorigin>
Variable fonts
/* One file instead of multiple weights */ @font-face { font-family: 'Inter'; src: url('/fonts/Inter-Variable.woff2') format('woff2-variations'); font-weight: 100 900; font-display: swap; }
Caching strategy
Cache-Control headers
HTML (short or no cache)
Cache-Control: no-cache, must-revalidate
Static assets with hash (immutable)
Cache-Control: public, max-age=31536000, immutable
Static assets without hash
Cache-Control: public, max-age=86400, stale-while-revalidate=604800
API responses
Cache-Control: private, max-age=0, must-revalidate
Service worker caching
// Cache-first for static assets self.addEventListener('fetch', (event) => { if (event.request.destination === 'image' || event.request.destination === 'style' || event.request.destination === 'script') { event.respondWith( caches.match(event.request).then((cached) => { return cached || fetch(event.request).then((response) => { const clone = response.clone(); caches.open('static-v1').then((cache) => cache.put(event.request, clone)); return response; }); }) ); } });
Runtime performance
Avoid layout thrashing
// ❌ Forces multiple reflows elements.forEach(el => { const height = el.offsetHeight; // Read el.style.height = height + 10 + 'px'; // Write });
// ✅ Batch reads, then batch writes const heights = elements.map(el => el.offsetHeight); // All reads elements.forEach((el, i) => { el.style.height = heights[i] + 10 + 'px'; // All writes });
Debounce expensive operations
function debounce(fn, delay) { let timeout; return (...args) => { clearTimeout(timeout); timeout = setTimeout(() => fn(...args), delay); }; }
// Debounce scroll/resize handlers window.addEventListener('scroll', debounce(handleScroll, 100));
Use requestAnimationFrame
// ❌ May cause jank setInterval(animate, 16);
// ✅ Synced with display refresh function animate() { // Animation logic requestAnimationFrame(animate); } requestAnimationFrame(animate);
Virtualize long lists
// For lists > 100 items, render only visible items // Use libraries like react-window, vue-virtual-scroller, or native CSS: .virtual-list { content-visibility: auto; contain-intrinsic-size: 0 50px; /* Estimated item height */ }
Third-party scripts
Load strategies
// ❌ Blocks main thread <script src="https://analytics.example.com/script.js"></script>
// ✅ Async loading <script async src="https://analytics.example.com/script.js"></script>
// ✅ Delay until interaction <script> document.addEventListener('DOMContentLoaded', () => { const observer = new IntersectionObserver((entries) => { if (entries[0].isIntersecting) { const script = document.createElement('script'); script.src = 'https://widget.example.com/embed.js'; document.body.appendChild(script); observer.disconnect(); } }); observer.observe(document.querySelector('#widget-container')); }); </script>
Facade pattern
<!-- Show static placeholder until interaction --> <div class="youtube-facade" data-video-id="abc123" onclick="loadYouTube(this)"> <img src="/thumbnails/abc123.jpg" alt="Video title"> <button aria-label="Play video">▶</button> </div>
Measurement
Key metrics
Metric Target Tool
LCP < 2.5s Lighthouse, CrUX
FCP < 1.8s Lighthouse
Speed Index < 3.4s Lighthouse
TBT < 200ms Lighthouse
TTI < 3.8s Lighthouse
Testing commands
Lighthouse CLI
npx lighthouse https://example.com --output html --output-path report.html
Web Vitals library
import {onLCP, onINP, onCLS} from 'web-vitals'; onLCP(console.log); onINP(console.log); onCLS(console.log);
References
For Core Web Vitals specific optimizations, see Core Web Vitals.