How to Optimize Web Performance: Core Web Vitals and Beyond
Achieve sub-2-second load times and pass Core Web Vitals. Covers image optimization, lazy loading, code splitting, CDN strategy, and Lighthouse auditing.
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.
The Three Core Web Vitals
| Metric | What It Measures | Good | Needs Work | Poor |
|---|---|---|---|---|
| LCP (Largest Contentful Paint) | Loading speed | ≤ 2.5s | 2.5-4.0s | > 4.0s |
| INP (Interaction to Next Paint) | Responsiveness | ≤ 200ms | 200-500ms | > 500ms |
| CLS (Cumulative Layout Shift) | Visual stability | ≤ 0.1 | 0.1-0.25 | > 0.25 |
Step 1: Audit Current Performance
# Lighthouse CLI audit
npx lighthouse https://yoursite.com \
--output=json --output=html \
--output-path=./lighthouse-report \
--chrome-flags="--headless"
# PageSpeed Insights API
curl "https://www.googleapis.com/pagespeedonline/v5/runPagespeed?url=https://yoursite.com&strategy=mobile&key=$API_KEY" \
| jq '.lighthouseResult.categories.performance.score'
Step 2: Optimize Images (Biggest Impact)
Images typically account for 50-80% of page weight.
2.1 Use Modern Formats
<!-- WebP with JPEG fallback -->
<picture>
<source srcset="hero.webp" type="image/webp">
<source srcset="hero.avif" type="image/avif">
<img src="hero.jpg" alt="Hero image"
width="1200" height="600"
loading="lazy"
decoding="async">
</picture>
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
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>
);
}
Step 4: Optimize CSS Delivery
<!-- Inline critical CSS -->
<style>
/* Critical above-the-fold styles */
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>
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";
}
CDN Checklist
| Asset Type | Cache Duration | CDN Strategy |
|---|---|---|
| Static assets (JS, CSS, images) | 1 year (fingerprinted) | Edge cache |
| HTML pages | 10 minutes | Stale-while-revalidate |
| API responses | 0 (no-store) | Origin only |
| Fonts | 1 year | Edge cache |
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;
}
/* 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 */
}
Step 7: Preload Critical Resources
<head>
<!-- Preload LCP image -->
<link rel="preload" as="image" href="hero.webp" fetchpriority="high">
<!-- Preconnect to third-party origins -->
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://cdn.yoursite.com">
<!-- DNS prefetch for analytics -->
<link rel="dns-prefetch" href="https://www.google-analytics.com">
</head>
Performance Budget
Set and enforce performance budgets:
| Metric | Budget | Enforcement |
|---|---|---|
| Total page weight | < 1.5 MB | CI/CD check |
| JavaScript bundle | < 300 KB (gzipped) | Webpack analyzer |
| 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
Performance Checklist
- Lighthouse score > 90 on mobile
- Images in WebP/AVIF with responsive srcset
- Lazy loading on below-the-fold images
- Code splitting for route-based chunks
- Critical CSS inlined, non-critical deferred
- CDN with 1-year cache on fingerprinted assets
- All media has explicit width/height (no CLS)
- Fonts use
font-display: swap - Preload LCP image and preconnect third-party
- Performance budget enforced in CI/CD
:::note[Source] This guide is derived from operational intelligence at Garnet Grid Consulting. For performance audits, visit garnetgrid.com. :::