Website Speed Optimization: A Complete Guide to Core Web Vitals
Slow websites lose rankings, lose customers, and lose revenue. This complete guide covers Core Web Vitals, image optimization, lazy loading, CDN configuration, and every technique that actually moves the needle in 2026.
A one-second delay in page load time reduces conversions by 7%. A site that loads in 1 second converts 3x better than a site that loads in 5 seconds. These numbers haven't changed much since Amazon first published them — but the stakes have gotten higher. In 2026, Google uses Core Web Vitals as a direct ranking factor, turning your site's speed into an SEO asset or a liability.
Understanding Core Web Vitals in 2026
Google's Core Web Vitals are three metrics that measure real-world user experience. They're measured from actual user sessions (Chrome User Experience Report, or CrUX) — not synthetic lab tests. This distinction matters enormously: your developer machine on fiber isn't representative of a user on a mid-range phone on 4G.
LCP — Largest Contentful Paint
LCP measures how long it takes for the largest visible element (usually a hero image or headline) to appear on screen. Think of it as "when does the page feel loaded?"
- Good: Under 2.5 seconds
- Needs improvement: 2.5s – 4.0s
- Poor: Over 4.0 seconds
The LCP element is almost always an image. Your hero image optimization directly determines your LCP score.
INP — Interaction to Next Paint
INP replaced FID (First Input Delay) in 2024 and measures how responsive your page is to all user interactions throughout their session — clicks, taps, keyboard input. If clicking a button feels sluggish, your INP is poor.
- Good: Under 200ms
- Needs improvement: 200ms – 500ms
- Poor: Over 500ms
Heavy JavaScript is the primary cause of poor INP. Every script that runs on the main thread competes with the browser's ability to respond to user input.
CLS — Cumulative Layout Shift
CLS measures visual stability — how much elements move around as the page loads. That frustrating experience of clicking a button right as an ad loads and bumps it down? That's CLS.
- Good: Under 0.1
- Needs improvement: 0.1 – 0.25
- Poor: Over 0.25
Always set explicit width and height on images and iframes. Reserve space for ads and embeds. These two rules alone solve 80% of CLS issues.
Image Optimization: The Highest-ROI Fix
Images account for 50-70% of the total bytes downloaded on a typical webpage. Fixing images is almost always the single highest-ROI performance optimization you can make.
Use Modern Formats
WebP provides 25-35% smaller file sizes than JPEG at the same quality. AVIF provides 50% smaller sizes than JPEG. Use AVIF with WebP fallback:
<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">
</picture>
In Next.js, this is automatic. The <Image> component handles format selection, resizing, and lazy loading:
import Image from 'next/image';
// Automatically serves WebP/AVIF, correct size per device,
// lazy loads, prevents CLS with width/height
<Image
src="/hero.jpg"
alt="Hero image"
width={1200}
height={600}
priority // Add for above-the-fold images (improves LCP)
quality={85}
/>
Responsive Images
Don't serve a 2000px image to a 375px mobile screen. Use srcset to let the browser pick the right size:
<img
src="product-800.jpg"
srcset="product-400.jpg 400w,
product-800.jpg 800w,
product-1200.jpg 1200w"
sizes="(max-width: 600px) 400px,
(max-width: 1200px) 800px,
1200px"
alt="Product photo"
width="800"
height="600"
>
Lazy Loading Non-Critical Images
Images below the fold don't need to load immediately. Native lazy loading is supported in all modern browsers:
<img src="blog-thumbnail.jpg" loading="lazy" alt="Article thumbnail" width="400" height="300">
Important: never put loading="lazy" on your LCP element (the above-the-fold hero image). This is a common mistake that destroys your LCP score. Use loading="eager" or omit the attribute entirely for above-the-fold images.
Image Compression
Quality 80-85 for JPEG/WebP is indistinguishable from quality 100 to most users but 40-60% smaller. Tools for batch compression:
- Sharp (Node.js): Fastest option for server-side processing
- Squoosh CLI: Google's tool, excellent AVIF support
- ImageMagick: Universal, scriptable, available on any server
# Compress an entire directory to WebP with Sharp
import sharp from 'sharp';
import { readdir } from 'fs/promises';
const images = await readdir('./public/images');
for (const file of images.filter(f => f.match(/.(jpg|jpeg|png)$/))) {
await sharp(`./public/images/${file}`)
.webp({ quality: 82 })
.toFile(`./public/images/${file.replace(/.[^.]+$/, '.webp')}`);
}
JavaScript Optimization
JavaScript is the #1 cause of poor INP scores. Every kilobyte of JavaScript must be parsed, compiled, and executed — all on the main thread.
Code Splitting
Don't load everything upfront. Split your JavaScript bundle so each page only loads what it needs:
// Instead of importing a heavy library at the top of every page
import { HeavyComponent } from 'heavy-library';
// Lazy-load it only when needed
import dynamic from 'next/dynamic';
const HeavyComponent = dynamic(() => import('./HeavyComponent'), {
loading: () => <Skeleton />,
ssr: false, // Skip server rendering for client-only components
});
Defer Non-Critical Scripts
Third-party scripts (analytics, chat widgets, social buttons) are notorious performance killers. Load them after the critical content:
<!-- Bad: blocks rendering -->
<script src="https://analytics.example.com/script.js"></script>
<!-- Good: loads asynchronously after page parse -->
<script src="https://analytics.example.com/script.js" defer></script>
<!-- Better: loads without blocking, executes when downloaded -->
<script src="https://analytics.example.com/script.js" async></script>
Audit Your Dependencies
Run npx bundlephobia or check bundlephobia.com before adding any npm package. Common oversized culprits:
moment.js(230KB) → replace withdate-fns(tree-shakeable, ~15KB) ordayjs(2KB)lodash(70KB) → import individual functions:import debounce from 'lodash/debounce'axios(45KB) → nativefetchworks in all modern environments- Full icon libraries → import only the icons you use
CDN Configuration
A Content Delivery Network serves your assets from the server geographically closest to each user. For a Montreal business serving Canadian users, this might seem less important — but CDNs do more than just move assets closer.
What a CDN Gives You
- Reduced TTFB: Static assets served from edge nodes 10-50ms away instead of your origin server 100-300ms away
- DDoS protection: Edge nodes absorb attack traffic before it reaches your server
- HTTP/3 support: Modern CDNs support QUIC/HTTP3 which dramatically improves performance on lossy connections (mobile networks)
- Automatic compression: Brotli compression is typically 20% smaller than gzip — most CDNs handle this automatically
- Image CDN features: Cloudflare Images, Cloudinary, and similar services resize and convert images on-the-fly from a single source file
CDN Cache Headers
Without proper cache headers, a CDN won't cache your assets and every request hits your origin:
# nginx — cache static assets aggressively
location ~* .(js|css|png|jpg|jpeg|gif|ico|webp|avif|woff2)$ {
expires 1y;
add_header Cache-Control "public, immutable";
}
# HTML — short cache with revalidation
location ~* .html$ {
expires 1h;
add_header Cache-Control "public, must-revalidate";
}
Use content hashing in your build system so asset filenames change when content changes (main.a3f7b2.js). This lets you cache forever without worrying about stale assets.
Critical Rendering Path
The critical rendering path is the sequence of steps the browser takes to convert HTML, CSS, and JavaScript into pixels on screen. Optimizing it is about removing anything that delays that sequence.
Inline Critical CSS
CSS is render-blocking — the browser won't paint anything until it has downloaded and parsed all CSS. Inlining the styles needed for above-the-fold content eliminates this render block:
<head>
<!-- Critical CSS inlined — no network request required -->
<style>
body { margin: 0; font-family: system-ui, sans-serif; }
.hero { min-height: 100vh; background: #050508; }
.hero h1 { color: #fff; font-size: clamp(2rem, 5vw, 4rem); }
</style>
<!-- Non-critical CSS loaded asynchronously -->
<link rel="preload" href="/styles/main.css" as="style" onload="this.onload=null;this.rel='stylesheet'">
<noscript><link rel="stylesheet" href="/styles/main.css"></noscript>
</head>
Font Loading Strategy
Custom web fonts cause layout shift (CLS) and delayed text rendering. The optimal strategy:
<!-- Preconnect to font provider -->
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<!-- Preload the specific font file -->
<link rel="preload" href="/fonts/inter-var.woff2" as="font" type="font/woff2" crossorigin>
/* Use font-display: swap to show text immediately with fallback */
@font-face {
font-family: 'Inter';
src: url('/fonts/inter-var.woff2') format('woff2');
font-display: swap; /* Show fallback font until custom font loads */
font-weight: 100 900;
}
Better yet: self-host your fonts. No cross-origin DNS lookup, no third-party privacy implications, full control over caching.
Server Response Time (TTFB)
Before the browser can do anything with your page, it needs to receive the first byte from your server. High Time to First Byte (TTFB) handicaps every other metric.
- Good TTFB: Under 800ms
- Acceptable: 800ms – 1800ms
- Poor: Over 1800ms
Common causes of high TTFB:
- Slow database queries (add indexes, cache results)
- Server far from users (use a CDN edge, or pick a server region close to your audience)
- No page caching (static generation or full-page cache dramatically reduces TTFB)
- Blocking API calls during server rendering (parallelize with
Promise.all())
// Bad: sequential API calls
const user = await getUser(id);
const orders = await getOrders(id);
const recommendations = await getRecommendations(id);
// Good: parallel API calls
const [user, orders, recommendations] = await Promise.all([
getUser(id),
getOrders(id),
getRecommendations(id),
]);
The Performance Audit Workflow
Run this process monthly, or before every significant release:
- PageSpeed Insights: Check both mobile and desktop scores. Use the field data (CrUX), not just lab data.
- Chrome DevTools Performance panel: Profile a page load, look for long tasks (over 50ms) and layout thrashing.
- WebPageTest: Run a filmstrip view to see exactly what the user sees at each 100ms interval.
- Lighthouse CI: Automate performance scoring in your CI pipeline so regressions are caught before deploy.
- Real User Monitoring: Tools like Vercel Analytics, Cloudflare Web Analytics, or Sentry Performance show real user experiences, not simulated ones.
Quick Wins Checklist
If you need to improve your scores fast, tackle these in order — each takes under an hour and makes a measurable difference:
- Convert images to WebP/AVIF and add proper dimensions (fixes LCP + CLS)
- Add
loading="lazy"to all below-fold images (reduces initial payload) - Enable Brotli compression on your server (20-30% smaller responses)
- Add
deferto all non-critical scripts (fixes INP) - Preload your LCP image:
<link rel="preload" as="image" href="/hero.webp"> - Set explicit width and height on all images (fixes CLS)
- Move Google Fonts to self-hosted (eliminates cross-origin DNS lookup)
- Enable HTTP/2 on your server if you haven't (multiplexed connections, free performance)
Speed optimization isn't a one-time project. It's an ongoing discipline. But the compound returns are real: faster sites rank higher, convert better, and cost less to host as they scale. Every millisecond you shave off is money in the bank.