Why Your Web Vitals Are Suffering (And How to Fix Them)

Back to Blog
Lukas Gisder-Dubé
7 min read
cdnweb-performancecore-web-vitalslighthouseseo

Why Your Web Vitals Are Suffering (And How to Fix Them)

I ran Lighthouse on a client's landing page last week. Performance score: 34. The page looked fine to me on my fiber connection, but the numbers told a different story. LCP was 8.2 seconds. Users on slower connections were having a miserable experience.

The culprit? A 3MB hero image served from the origin server, plus a dozen unoptimized product photos loading all at once. Classic asset delivery problems masquerading as "the site is just slow."

Let me show you how to diagnose and fix the most common Web Vitals issues related to asset delivery.

Understanding the Three Core Web Vitals

Before we fix anything, let's understand what we're measuring:

LCP (Largest Contentful Paint) measures when the main content becomes visible. For most pages, this is the hero image or a large text block. Target: under 2.5 seconds.

INP (Interaction to Next Paint) measures responsiveness when users click or tap. Large JavaScript bundles and render-blocking resources hurt this. Target: under 200ms.

CLS (Cumulative Layout Shift) measures visual stability—how much the page jumps around as it loads. Images without dimensions are a major cause. Target: under 0.1.

Diagnosing Your Issues

Start with PageSpeed Insights. It shows both lab data (simulated under controlled conditions) and field data (real-world user experiences). Pay attention to field data—that's what Google uses for ranking. Lab data is useful for debugging, but field data reflects what your actual users experience.

Look for these red flags:

Slow LCP: Check if your largest image is:

  • Hosted on your origin server (not a CDN)
  • Uncompressed or in an inefficient format (PNG instead of WebP)
  • Too large for its display size
  • Not preloaded

Poor INP: Look for:

  • Large JavaScript bundles blocking the main thread
  • Too many synchronous scripts
  • Heavy third-party widgets

High CLS: Watch for:

  • Images without width/height attributes
  • Ads or embeds that load late
  • Web fonts causing text reflow

Fix #1: Move Images to a CDN

This is the highest-impact fix for most sites. When images load from your origin server, users far from that server experience significant delays.

Here's a quick migration script. You'll need Node.js, sharp for image processing, and an easyCDN account with a secret key—install with npm install @easycdn/server sharp:

ts
import { createClient } from '@easycdn/server'
import { readdir, readFile } from 'fs/promises'
import { join, extname } from 'path'
import sharp from 'sharp'

const client = createClient({
  secretKey: process.env.EASYCDN_SECRET_KEY!,
})

async function migrateImages(sourceDir: string) {
  const files = await readdir(sourceDir)

  for (const file of files) {
    const ext = extname(file).toLowerCase()
    if (!['.jpg', '.jpeg', '.png', '.webp'].includes(ext)) continue

    const buffer = await readFile(join(sourceDir, file))

    try {
      // Convert to WebP for better compression
      const optimized = await sharp(buffer)
        .webp({ quality: 85 })
        .toBuffer()

      const result = await client.upload(optimized, {
        fileName: file.replace(/\.[^.]+$/, '.webp'),
      })

      console.log(`${file} -> ${result.asset.url}`)
    } catch (error) {
      console.error(`Failed: ${file}`, error)
    }
  }
}

migrateImages('./public/images')

Converting to WebP cuts file sizes by 25-35%.

Fix #2: Preload Your LCP Image

Once your hero image is on a CDN, tell the browser to fetch it early:

html
<head>
  <link
    rel="preload"
    as="image"
    href="https://cdn.easycdn.co/your-project/hero.webp"
    fetchpriority="high"
  />
</head>

This can shave hundreds of milliseconds off LCP by starting the download before the browser discovers the image in the HTML.

Fix #3: Add Dimensions to Every Image

Images without width and height cause layout shifts as they load. The browser doesn't know how much space to reserve, so content jumps when images finally render.

tsx
// Bad - causes CLS
<img src="product.webp" alt="Product" />

// Good - prevents CLS
<img
  src="product.webp"
  alt="Product"
  width={800}
  height={600}
/>

If you don't know the dimensions, use aspect-ratio in CSS:

css
.product-image {
  aspect-ratio: 4 / 3;
  width: 100%;
  height: auto;
}

Fix #4: Lazy Load Below-the-Fold Images

Every image that loads competes for bandwidth with your LCP element. Defer images users won't see immediately:

tsx
// Above the fold - load immediately
<img
  src="https://cdn.easycdn.co/hero.webp"
  alt="Hero"
  fetchPriority="high"
  width={1200}
  height={600}
/>

// Below the fold - lazy load
<img
  src="https://cdn.easycdn.co/product.webp"
  alt="Product"
  loading="lazy"
  width={400}
  height={300}
/>

Native lazy loading is supported in all modern browsers and requires zero JavaScript.

Fix #5: Optimize Image Sizes

Don't serve a 4000px image when users only see it at 800px. Create appropriately sized versions. If your CDN supports server-side transforms, you can generate multiple sizes during upload—with easyCDN, use the transform option:

ts
const sizes = [400, 800, 1200]

const variants = await Promise.all(
  sizes.map(async (width) => {
    const result = await client.upload(imageBuffer, {
      fileName: `hero-${width}.webp`,
      transform: { image: { width, format: 'webp', quality: 85 } },
    })
    return { width, url: result.asset.url }
  })
)

Then use srcset so the browser picks the best size based on viewport and device pixel ratio:

tsx
<img
  src={variants[1].url}
  srcSet={variants.map(({ url, width }) => `${url} ${width}w`).join(', ')}
  sizes="(max-width: 600px) 100vw, 800px"
  alt="Hero image"
  width={800}
  height={400}
/>

Fix #6: Defer Non-Critical JavaScript

INP suffers when large JavaScript bundles block the main thread. Defer scripts that aren't needed for initial render:

html
<!-- Critical scripts load normally -->
<script src="critical.js"></script>

<!-- Non-critical scripts are deferred -->
<script defer src="analytics.js"></script>
<script defer src="chat-widget.js"></script>

The defer attribute tells the browser to download the script in parallel but wait to execute until after the HTML is parsed. This keeps the main thread free for user interactions.

Fix #7: Set Proper Cache Headers

Long cache times mean returning visitors load instantly. CDNs typically handle this automatically, but verify your headers in the Network tab:

text
Cache-Control: public, max-age=31536000, immutable

For assets with hashes in the filename (like app.a3f2b1.js), you can cache forever. The filename changes when content changes.

Measuring Your Progress

After each fix, run PageSpeed Insights again. You should see improvements in:

  • LCP: CDN delivery + preloading
  • CLS: Image dimensions + reserved space
  • Total Blocking Time: Reduced with lazy loading

For ongoing monitoring, set up a performance budget in your CI:

bash
# Using Lighthouse CI
lhci autorun --assert.preset=lighthouse:recommended

Real-World Impact

After implementing these fixes on the client site I mentioned:

MetricBeforeAfter
LCP8.2s1.8s
CLS0.320.04
Performance Score3489

The biggest gains came from moving images to a CDN and adding proper dimensions. Total time invested: about 2 hours.

Quick Wins Checklist

Here's a prioritized list for immediate impact:

  1. Move your LCP image to a CDN
  2. Add fetchpriority="high" to your hero image
  3. Add width/height to all images
  4. Convert images to WebP format
  5. Add loading="lazy" to below-fold images
  6. Remove render-blocking resources from <head>
  7. Test with PageSpeed Insights

Wrapping Up

Most Web Vitals issues trace back to asset delivery. Large images, slow servers, missing dimensions—these are fixable problems that don't require rewriting your app.

The pattern is simple: serve optimized images from edge locations, tell the browser what to prioritize, and reserve space for content that loads asynchronously.

Your users (and Google) will notice the difference immediately.

If you're tired of wrestling with image optimization, easyCDN handles hosting and global delivery out of the box. Upload your images, swap out the URLs, and let the CDN do the heavy lifting. The free tier is enough to test the impact on your own site.

Ready to host your assets?

Create your free account and start serving your assets in minutes.

No credit card required • Get started in under 2 minutes