A 1-second delay in page load reduces conversions by 7%. A 3-second delay loses 53% of mobile visitors. Google uses Core Web Vitals as a ranking signal. Performance isn’t optional — it’s a business metric that directly affects revenue, SEO rankings, and user experience. This guide covers practical techniques to achieve sub-2-second load times and pass all Core Web Vitals, from quick wins to advanced optimizations.
The 80/20 rule of web performance: image optimization and code splitting solve 80% of performance problems. Start there before touching anything else.
The Three Core Web Vitals
| Metric | What It Measures | Good | Needs Work | Poor |
|---|
| LCP (Largest Contentful Paint) | Loading speed — when does the main content appear? | ≤ 2.5s | 2.5-4.0s | > 4.0s |
| INP (Interaction to Next Paint) | Responsiveness — how fast does the page react to clicks? | ≤ 200ms | 200-500ms | > 500ms |
| CLS (Cumulative Layout Shift) | Visual stability — does the page jump around? | ≤ 0.1 | 0.1-0.25 | > 0.25 |
What Causes Each Problem
| Metric | Common Causes | Quick Wins |
|---|
| LCP | Large images, render-blocking CSS/JS, slow server | Compress images, preload LCP element, CDN |
| INP | Heavy JavaScript, long tasks blocking main thread | Code splitting, defer non-critical JS, web workers |
| CLS | Images without dimensions, dynamic content injection, font swaps | Set width/height, reserve ad slots, font-display: swap |
# Lighthouse CLI audit
npx lighthouse https://yoursite.com \
--output=json --output=html \
--output-path=./lighthouse-report \
--chrome-flags="--headless"
# PageSpeed Insights API (field + lab data)
curl "https://www.googleapis.com/pagespeedonline/v5/runPagespeed?url=https://yoursite.com&strategy=mobile&key=$API_KEY" \
| jq '.lighthouseResult.categories.performance.score'
# WebPageTest (detailed waterfall analysis)
# Visit webpagetest.org for filmstrip view + server timing
Lab Data vs Field Data
| Data Type | Source | Shows | Use For |
|---|
| Lab data | Lighthouse, WebPageTest | Controlled test results | Debugging, CI/CD gates |
| Field data | Chrome UX Report, RUM | Real user experience | SEO impact, actual performance |
Step 2: Optimize Images (Biggest Impact)
Images typically account for 50-80% of page weight. Fixing images alone can cut load times in half.
<!-- WebP with JPEG fallback, AVIF for maximum compression -->
<picture>
<source srcset="hero.avif" type="image/avif">
<source srcset="hero.webp" type="image/webp">
<img src="hero.jpg" alt="Hero image"
width="1200" height="600"
loading="lazy"
decoding="async">
</picture>
| Format | Compression | Browser Support | Use When |
|---|
| JPEG | Good | Universal | Fallback only |
| WebP | 25-35% smaller than JPEG | 97%+ | Default for most images |
| AVIF | 50% smaller than JPEG | 92%+ | Hero images, high-quality photos |
| SVG | Vector (smallest for icons) | Universal | Logos, icons, illustrations |
2.2 Responsive Images
<img
srcset="
hero-400.webp 400w,
hero-800.webp 800w,
hero-1200.webp 1200w,
hero-1600.webp 1600w
"
sizes="(max-width: 600px) 400px,
(max-width: 1024px) 800px,
1200px"
src="hero-800.webp"
alt="Hero image"
width="1200"
height="600"
loading="lazy"
>
2.3 Batch Optimization Script
# Convert all images to WebP with quality 80
for img in *.{jpg,png}; do
cwebp -q 80 "$img" -o "${img%.*}.webp"
done
# Generate responsive sizes
for img in *.webp; do
for size in 400 800 1200 1600; do
convert "$img" -resize "${size}x" "${img%.*}-${size}.webp"
done
done
# AVIF conversion for hero images
for img in hero*.{jpg,png}; do
avifenc --min 20 --max 30 "$img" "${img%.*}.avif"
done
Step 3: Implement Code Splitting
// React — dynamic imports for route-based splitting
import { lazy, Suspense } from 'react';
const Dashboard = lazy(() => import('./pages/Dashboard'));
const Settings = lazy(() => import('./pages/Settings'));
const Reports = lazy(() => import('./pages/Reports'));
function App() {
return (
<Suspense fallback={<LoadingSpinner />}>
<Routes>
<Route path="/dashboard" element={<Dashboard />} />
<Route path="/settings" element={<Settings />} />
<Route path="/reports" element={<Reports />} />
</Routes>
</Suspense>
);
}
Bundle Analysis
# Webpack Bundle Analyzer — find what's bloated
npx webpack-bundle-analyzer dist/stats.json
# Next.js built-in analyzer
ANALYZE=true npm run build
# Vite — rollup-plugin-visualizer
# Shows exactly which dependencies eat your bundle budget
Common Bundle Bloat Offenders
| Library | Typical Size (gzipped) | Alternative |
|---|
| moment.js | 72 KB | day.js (2 KB) |
| lodash (full) | 72 KB | lodash-es (tree-shake) or native JS |
| chart.js | 65 KB | chart.js/auto (tree-shake) |
| date-fns (full) | 40 KB | Import specific functions |
Step 4: Optimize CSS Delivery
<!-- Inline critical CSS (above-the-fold styles) -->
<style>
/* Critical above-the-fold styles only */
body { margin: 0; font-family: system-ui; }
.hero { min-height: 100vh; display: grid; place-items: center; }
.nav { position: sticky; top: 0; z-index: 100; }
</style>
<!-- Defer 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>
# Extract critical CSS automatically
npx critical https://yoursite.com \
--base dist/ \
--inline \
--width 1300 --height 900
Step 5: CDN and Caching Strategy
# Nginx caching headers
location ~* \.(js|css|png|webp|avif|woff2)$ {
expires 1y;
add_header Cache-Control "public, immutable";
}
location ~* \.html$ {
expires 10m;
add_header Cache-Control "public, must-revalidate";
}
# Compression
gzip on;
gzip_types text/css application/javascript application/json image/svg+xml;
gzip_min_length 256;
# Brotli (better compression than gzip)
brotli on;
brotli_types text/css application/javascript application/json;
CDN Caching Rules
| Asset Type | Cache Duration | Strategy | Invalidation |
|---|
| Static assets (JS, CSS, images) | 1 year | Fingerprinted filenames (app.a3b8c.js) | New build = new filename |
| HTML pages | 10 minutes | Stale-while-revalidate | CDN purge on deploy |
| API responses | 0 (no-store) | Origin only | N/A |
| Fonts | 1 year | Immutable (self-hosted preferred) | New filename |
Step 6: Fix CLS (Layout Shift)
/* Always specify dimensions for images and videos */
img, video {
width: 100%;
height: auto;
aspect-ratio: 16 / 9; /* Reserves space before load */
}
/* Reserve space for ads */
.ad-slot {
min-height: 250px;
background: #f0f0f0; /* Visual placeholder */
}
/* Prevent font swap layout shift */
@font-face {
font-family: 'CustomFont';
src: url('font.woff2') format('woff2');
font-display: swap;
size-adjust: 105%; /* Match fallback font metrics to reduce shift */
}
Step 7: Preload Critical Resources
<head>
<!-- Preload LCP image (highest priority) -->
<link rel="preload" as="image" href="hero.webp" fetchpriority="high">
<!-- Preconnect to third-party origins (DNS + TLS handshake) -->
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://cdn.yoursite.com">
<!-- DNS prefetch for less critical third parties -->
<link rel="dns-prefetch" href="https://www.google-analytics.com">
<link rel="dns-prefetch" href="https://www.googletagmanager.com">
</head>
Resource Priority Hints
| Hint | Use For | Impact |
|---|
fetchpriority="high" | LCP image, critical font | Faster above-the-fold rendering |
fetchpriority="low" | Below-fold images, non-critical scripts | Prevents contention with critical resources |
loading="lazy" | Below-fold images | Defers download until near viewport |
loading="eager" | Above-fold LCP image | Downloads immediately (default) |
Set and enforce performance budgets in CI/CD:
| Metric | Budget | Enforcement |
|---|
| Total page weight | < 1.5 MB | CI/CD check (bundle analyzer) |
| JavaScript bundle | < 300 KB (gzipped) | Webpack/Vite config |
| CSS bundle | < 50 KB (gzipped) | Build check |
| LCP | < 2.5 seconds | Lighthouse CI |
| INP | < 200ms | Real User Monitoring |
| CLS | < 0.1 | Lighthouse CI |
# Lighthouse CI in GitHub Actions
npx @lhci/cli collect --url="https://staging.yoursite.com"
npx @lhci/cli assert \
--preset=lighthouse:recommended \
--assert.maxSize=1572864 # 1.5 MB
Set concrete budgets and enforce them in CI/CD:
| Metric | Target | Acceptable | Unacceptable |
|---|
| LCP (Largest Contentful Paint) | Under 1.5s | Under 2.5s | Over 2.5s |
| FID (First Input Delay) | Under 50ms | Under 100ms | Over 100ms |
| CLS (Cumulative Layout Shift) | Under 0.05 | Under 0.1 | Over 0.1 |
| Total JS bundle size | Under 200KB | Under 350KB | Over 500KB |
| Total page weight | Under 1MB | Under 2MB | Over 3MB |
| Time to Interactive | Under 3s | Under 5s | Over 5s |
| Number of HTTP requests | Under 30 | Under 50 | Over 80 |
Run these checks on any page to find the biggest wins:
- Images — Are they using modern formats (WebP or AVIF)? Are they lazy-loaded below the fold?
- JavaScript — Is the bundle tree-shaken? Are unused libraries removed?
- CSS — Is critical CSS inlined? Is unused CSS purged?
- Fonts — Are you preloading? Using font-display swap? Subsetting?
- Third-party scripts — How many analytics or tracking scripts are loaded? Can any be deferred?
:::note[Source]
This guide is derived from operational intelligence at Garnet Grid Consulting. For performance audits, visit garnetgrid.com.
:::