Performance isn't optional—it's a feature. Slow sites lose users, hurt SEO, and frustrate developers. Understanding performance fundamentals helps you build fast sites from the start rather than optimizing later.
Core Web Vitals#
LCP (Largest Contentful Paint)#
Time until the largest content element is visible.
Target: < 2.5 seconds
Common culprits:
- Large unoptimized images
- Slow server response
- Render-blocking resources
- Client-side rendering
Fixes:
1<!-- Preload critical images -->
2<link rel="preload" as="image" href="hero.jpg">
3
4<!-- Use responsive images -->
5<img
6 srcset="hero-400.jpg 400w, hero-800.jpg 800w, hero-1200.jpg 1200w"
7 sizes="(max-width: 600px) 400px, (max-width: 1200px) 800px, 1200px"
8 src="hero-800.jpg"
9 alt="Hero image"
10>
11
12<!-- Modern formats -->
13<picture>
14 <source srcset="hero.avif" type="image/avif">
15 <source srcset="hero.webp" type="image/webp">
16 <img src="hero.jpg" alt="Hero">
17</picture>FID/INP (Interaction to Next Paint)#
Time from user interaction to browser response.
Target: < 200ms
Common culprits:
- Long JavaScript tasks
- Heavy main thread work
- Synchronous operations
Fixes:
1// Break up long tasks
2function processLargeArray(items) {
3 const chunkSize = 100;
4 let index = 0;
5
6 function processChunk() {
7 const chunk = items.slice(index, index + chunkSize);
8 chunk.forEach(processItem);
9 index += chunkSize;
10
11 if (index < items.length) {
12 // Yield to browser
13 requestIdleCallback(processChunk);
14 }
15 }
16
17 processChunk();
18}
19
20// Use Web Workers for heavy computation
21const worker = new Worker('heavy-computation.js');
22worker.postMessage(data);
23worker.onmessage = (e) => updateUI(e.data);CLS (Cumulative Layout Shift)#
Visual stability—how much the page shifts unexpectedly.
Target: < 0.1
Common culprits:
- Images without dimensions
- Dynamically injected content
- Web fonts causing FOUT/FOIT
Fixes:
1<!-- Always specify dimensions -->
2<img src="photo.jpg" width="800" height="600" alt="Photo">
3
4<!-- Or use aspect-ratio -->
5<style>
6 .image-container {
7 aspect-ratio: 16 / 9;
8 width: 100%;
9 }
10</style>
11
12<!-- Reserve space for dynamic content -->
13<div class="ad-slot" style="min-height: 250px;">
14 <!-- Ad loads here -->
15</div>
16
17<!-- Prevent font swap shift -->
18<style>
19 @font-face {
20 font-family: 'MyFont';
21 font-display: optional; /* or swap with size-adjust */
22 size-adjust: 105%;
23 }
24</style>Resource Loading#
Critical Rendering Path#
1<!-- Critical CSS inline -->
2<style>
3 /* Above-the-fold styles only */
4 .header { ... }
5 .hero { ... }
6</style>
7
8<!-- Defer non-critical CSS -->
9<link rel="preload" href="styles.css" as="style" onload="this.rel='stylesheet'">
10
11<!-- Defer JavaScript -->
12<script src="app.js" defer></script>
13
14<!-- Async for independent scripts -->
15<script src="analytics.js" async></script>Resource Hints#
1<!-- DNS prefetch for third parties -->
2<link rel="dns-prefetch" href="//api.example.com">
3
4<!-- Preconnect for critical origins -->
5<link rel="preconnect" href="https://fonts.googleapis.com">
6
7<!-- Preload critical resources -->
8<link rel="preload" href="critical.js" as="script">
9<link rel="preload" href="hero.jpg" as="image">
10
11<!-- Prefetch for likely next page -->
12<link rel="prefetch" href="/next-page.html">
13
14<!-- Prerender (aggressive) -->
15<link rel="prerender" href="/likely-destination">JavaScript Optimization#
Bundle Optimization#
1// Dynamic imports for code splitting
2const HeavyComponent = lazy(() => import('./HeavyComponent'));
3
4// Route-based splitting
5const routes = [
6 {
7 path: '/dashboard',
8 component: lazy(() => import('./pages/Dashboard')),
9 },
10];
11
12// Conditional loading
13if (user.isPremium) {
14 const PremiumFeatures = await import('./PremiumFeatures');
15}Tree Shaking#
1// ❌ Imports entire library
2import _ from 'lodash';
3_.debounce(fn, 300);
4
5// ✅ Imports only what's needed
6import debounce from 'lodash/debounce';
7debounce(fn, 300);
8
9// ✅ Or use ES modules
10import { debounce } from 'lodash-es';Avoiding Main Thread Blocking#
1// Use requestIdleCallback for non-urgent work
2requestIdleCallback(() => {
3 // Analytics, preloading, etc.
4 trackEvent('page_view');
5});
6
7// Use IntersectionObserver for lazy loading
8const observer = new IntersectionObserver((entries) => {
9 entries.forEach(entry => {
10 if (entry.isIntersecting) {
11 loadImage(entry.target);
12 observer.unobserve(entry.target);
13 }
14 });
15});
16
17document.querySelectorAll('img[data-src]').forEach(img => {
18 observer.observe(img);
19});Image Optimization#
Modern Formats#
# Convert to WebP
cwebp -q 80 input.jpg -o output.webp
# Convert to AVIF
avifenc input.jpg output.avifResponsive Images#
1<img
2 src="image-800.jpg"
3 srcset="
4 image-400.jpg 400w,
5 image-800.jpg 800w,
6 image-1200.jpg 1200w
7 "
8 sizes="
9 (max-width: 400px) 400px,
10 (max-width: 800px) 800px,
11 1200px
12 "
13 loading="lazy"
14 decoding="async"
15 alt="Description"
16>Image CDN Usage#
<!-- Cloudinary example -->
<img src="https://res.cloudinary.com/demo/image/upload/w_800,f_auto,q_auto/sample.jpg">
<!-- Imgix example -->
<img src="https://example.imgix.net/image.jpg?w=800&auto=format,compress">Caching Strategy#
HTTP Caching#
1# Static assets - long cache
2location /static/ {
3 expires 1y;
4 add_header Cache-Control "public, immutable";
5}
6
7# HTML - short cache
8location / {
9 add_header Cache-Control "no-cache";
10}
11
12# API - no cache
13location /api/ {
14 add_header Cache-Control "no-store";
15}Service Worker Caching#
1// Cache static assets
2self.addEventListener('install', (event) => {
3 event.waitUntil(
4 caches.open('static-v1').then((cache) => {
5 return cache.addAll([
6 '/styles.css',
7 '/app.js',
8 '/offline.html',
9 ]);
10 })
11 );
12});
13
14// Network-first for API, cache-first for assets
15self.addEventListener('fetch', (event) => {
16 if (event.request.url.includes('/api/')) {
17 event.respondWith(networkFirst(event.request));
18 } else {
19 event.respondWith(cacheFirst(event.request));
20 }
21});Measuring Performance#
Lab Tools#
# Lighthouse CLI
lighthouse https://example.com --output html
# WebPageTest
webpagetest test https://example.comField Data#
1// Report Core Web Vitals
2import { onCLS, onFID, onLCP } from 'web-vitals';
3
4function sendToAnalytics(metric) {
5 analytics.track('web_vital', {
6 name: metric.name,
7 value: metric.value,
8 id: metric.id,
9 });
10}
11
12onCLS(sendToAnalytics);
13onFID(sendToAnalytics);
14onLCP(sendToAnalytics);Performance Budget#
1{
2 "budgets": [
3 {
4 "resourceType": "script",
5 "budget": 300
6 },
7 {
8 "resourceType": "total",
9 "budget": 1000
10 },
11 {
12 "metric": "first-contentful-paint",
13 "budget": 1500
14 }
15 ]
16}Common Pitfalls#
Third-Party Scripts#
1<!-- Load third parties after page load -->
2<script>
3 window.addEventListener('load', () => {
4 const script = document.createElement('script');
5 script.src = 'https://third-party.com/widget.js';
6 document.body.appendChild(script);
7 });
8</script>
9
10<!-- Or use facade pattern -->
11<div class="youtube-facade" data-video-id="abc123">
12 <img src="thumbnail.jpg" alt="Video">
13 <button>Play</button>
14</div>
15<script>
16 document.querySelector('.youtube-facade').addEventListener('click', () => {
17 // Load YouTube iframe only when clicked
18 });
19</script>Font Loading#
1/* Optimize font loading */
2@font-face {
3 font-family: 'MyFont';
4 src: url('font.woff2') format('woff2');
5 font-display: swap;
6 unicode-range: U+0000-00FF; /* Basic Latin only */
7}Conclusion#
Web performance is a continuous practice, not a one-time fix. Understand the metrics that matter, measure consistently, and optimize deliberately. The fundamentals—efficient loading, minimal JavaScript, optimized images, smart caching—provide the foundation for fast experiences.
Start with Core Web Vitals, establish performance budgets, and make performance part of your development workflow. Fast sites win.