Skip to content
Back to Blog
Performance11 min read

Core Web Vitals Optimization: 8 Fixes That Work

Eight technical fixes that improve LCP, FID, and CLS scores: from image optimization and lazy loading to font strategies and render-blocking resource elimination.

Written by Abdul AbrorTechnical Hosting Support Engineer
Core Web Vitals Optimization: 8 Fixes That Work
On this page

Google ranks sites partly on Core Web Vitals now, and tickets I see every week show the same patterns: bloated images, render-blocking CSS, JavaScript that runs before the page is even visible. The three metrics—Largest Contentful Paint (LCP), First Input Delay (FID), and Cumulative Layout Shift (CLS)—measure real user experience, not synthetic lab numbers. Fix them and you'll see faster load times, better engagement, and often a ranking bump.

Here are eight technical fixes that move the needle.

1. Optimize and serve next-gen image formats

Images usually account for the largest share of page weight. A 2 MB JPEG hero image will tank your LCP score instantly.

Convert images to WebP or AVIF. Both formats deliver the same visual quality at 25-35% smaller file sizes than JPEG or PNG. Most browsers support WebP; AVIF support is growing fast. Serve WebP with a JPEG fallback using the <picture> element:

<picture>
  <source srcset="hero.webp" type="image/webp">
  <img src="hero.jpg" alt="Hero image" width="1200" height="600">
</picture>

Always set explicit width and height attributes. The browser reserves space before the image loads, preventing layout shift.

For WordPress, plugins like ShortPixel or Imagify automate conversion and serve the right format per browser. On a VPS, install libwebp and use cwebp to batch-convert:

for img in *.jpg; do
  cwebp -q 85 "$img" -o "${img%.jpg}.webp"
done

Resize images to the exact dimensions needed. Don't serve a 3000×2000 photo when the layout never exceeds 800 pixels wide. Use ImageMagick or vips for bulk resizing:

vips resize input.jpg output.jpg 0.4

That scales the image to 40% of its original dimensions. Fast and efficient.

2. Implement native lazy loading

Loading every image on page load wastes bandwidth and delays LCP. Lazy loading defers off-screen images until the user scrolls near them.

The simplest approach is the native loading attribute:

<img src="screenshot.jpg" alt="Dashboard" loading="lazy" width="800" height="450">

Browsers handle the rest. No JavaScript library needed. For above-the-fold images—especially your LCP element—omit loading="lazy" or set loading="eager". Lazy-loading your hero image is a common mistake that wrecks LCP.

For older browsers that don't support native lazy loading, a small polyfill like loading-attribute-polyfill bridges the gap. Most traffic today uses modern browsers, so the native attribute alone covers the majority.

Iframes (YouTube embeds, maps) also accept loading="lazy". Defer third-party embeds that sit below the fold:

<iframe src="https://www.youtube.com/embed/xyz" loading="lazy" width="560" height="315"></iframe>

3. Choose a font loading strategy

Web fonts can block rendering for seconds while the browser downloads them, or cause a flash of unstyled text (FOUT) when the font finally arrives. Both hurt LCP and CLS.

Use font-display: swap in your @font-face declarations:

@font-face {
  font-family: 'Inter';
  src: url('/fonts/inter.woff2') format('woff2');
  font-weight: 400;
  font-display: swap;
}

The browser shows fallback text immediately, then swaps in the web font when ready. Users see content faster. The swap might cause a brief layout shift, but it's better than a blank screen.

Preload critical fonts in the <head>:

<link rel="preload" href="/fonts/inter.woff2" as="font" type="font/woff2" crossorigin>

Only preload one or two weights. Preloading every font variant delays other resources.

Self-host fonts when possible. Fetching from Google Fonts adds DNS lookup, connection, and TLS handshake time. Download the font files, serve them from your domain, and set long cache headers:

<FilesMatch "\.(woff2|woff)$">
  Header set Cache-Control "max-age=31536000, immutable"
</FilesMatch>

4. Split and defer JavaScript

Heavy JavaScript bundles delay interactivity and inflate FID. A single 800 KB bundle locks the main thread while parsing and executing, even if most functions aren't needed on initial load.

Code-split by route or component. Modern bundlers like Webpack and Vite support dynamic imports:

const module = await import('./heavy-feature.js');
module.init();

The browser fetches heavy-feature.js only when the code path reaches that import. The initial bundle shrinks significantly.

Defer non-essential scripts with the defer attribute:

<script src="analytics.js" defer></script>

Deferred scripts download in parallel but execute after HTML parsing finishes. For scripts that don't manipulate the DOM on load, this is often the right choice.

For JavaScript that truly isn't needed until user interaction (modals, carousels), load it on demand:

document.getElementById('open-modal').addEventListener('click', async () => {
  const { Modal } = await import('./modal.js');
  new Modal().show();
}, { once: true });

Minify and compress your bundles. Use Terser or esbuild for minification, and ensure your server sends JavaScript with Brotli or gzip compression.

5. Eliminate render-blocking CSS

CSS in the <head> blocks rendering until the browser downloads and parses it. A large stylesheet or slow server response keeps the screen blank.

Inline critical CSS—the styles needed for above-the-fold content—directly in the <head>:

<style>
  body { margin: 0; font-family: sans-serif; }
  .hero { background: #333; color: #fff; padding: 2rem; }
</style>
<link rel="stylesheet" href="/css/main.css" media="print" onload="this.media='all'">

The media="print" trick loads the full stylesheet without blocking render, then the onload handler switches it to all once loaded. A noscript fallback ensures it loads even if JavaScript is disabled:

<noscript><link rel="stylesheet" href="/css/main.css"></noscript>

Tools like Critical or Critters extract above-the-fold CSS automatically. For WordPress, plugins like WP Rocket or Autoptimize inline critical CSS and defer the rest.

Remove unused CSS with PurgeCSS or the coverage tool in Chrome DevTools. Many themes ship with thousands of lines of CSS for components never used on your site.

6. Prevent cumulative layout shift

CLS happens when visible elements move after the page loads. An ad suddenly appears, pushing content down. An image loads without reserved space. A font swap changes line height.

Always set dimensions on images, videos, and iframes:

<img src="photo.jpg" width="600" height="400" alt="Sunset">

Use aspect-ratio CSS for responsive containers:

.video-wrapper {
  aspect-ratio: 16 / 9;
  width: 100%;
}

Reserve space for dynamic content like ads or embeds. If an ad slot is 300×250 pixels, set a placeholder with those dimensions:

.ad-slot {
  width: 300px;
  height: 250px;
  background: #f0f0f0;
}

Avoid inserting content above existing elements unless triggered by user interaction. An announcement banner that pushes the entire page down scores badly on CLS.

Font swaps cause layout shift when the fallback and web font have different metrics. Use size-adjust, ascent-override, and descent-override in your @font-face to match the fallback font dimensions to the web font. Tools like Fallback Font Generator calculate these values for you.

7. Defer third-party scripts

Google Analytics, Facebook Pixel, chat widgets, and ad networks add significant overhead. Each script requires DNS lookup, connection, and execution time. Some block the main thread entirely.

Load third-party scripts asynchronously:

<script async src="https://www.googletagmanager.com/gtag/js?id=GA_MEASUREMENT_ID"></script>

The async attribute downloads the script in parallel and executes it as soon as it's ready, without blocking HTML parsing.

For scripts that aren't needed immediately, delay them until after the page is interactive. A simple pattern:

window.addEventListener('load', () => {
  setTimeout(() => {
    const script = document.createElement('script');
    script.src = 'https://example.com/widget.js';
    document.body.appendChild(script);
  }, 3000);
});

This waits three seconds after the load event before fetching the widget. Users interact with the page first; the widget arrives later.

Use a tag manager to control when and where third-party scripts fire. Google Tag Manager lets you trigger scripts on specific pages, scroll depth, or user actions instead of every page load.

For social embeds (Twitter, Instagram), load a static placeholder image first, then fetch the full embed on click. The page loads fast; the embed loads only if needed.

8. Reduce server response time (TTFB)

Time to First Byte (TTFB) is how long the browser waits for the server's first response byte. Slow TTFB delays LCP because the browser can't start rendering until it receives HTML.

TTFB over 600 ms is a red flag. Aim for under 200 ms.

Enable full-page caching. For WordPress, use WP Super Cache, W3 Total Cache, or Redis Object Cache. On Apache or Nginx, configure reverse proxy caching or use Varnish.

Nginx microcaching example:

proxy_cache_path /var/cache/nginx levels=1:2 keys_zone=microcache:10m max_size=1g inactive=1h;

server {
  location / {
    proxy_cache microcache;
    proxy_cache_valid 200 1m;
    proxy_cache_key "$scheme$host$request_uri";
    proxy_pass http://backend;
  }
}

This caches responses for one minute, serving static HTML instead of hitting PHP on every request.

Optimize database queries. Slow queries add hundreds of milliseconds to TTFB. Check your slow query log:

tail -f /var/log/mysql/slow-query.log

Add indexes to frequently queried columns, remove unnecessary JOINs, and limit the number of queries per page.

Use a CDN to serve content from edge locations near your users. Cloudflare, BunnyCDN, and KeyCDN cache static assets and often HTML, reducing latency significantly.

Upgrade your hosting if necessary. Shared hosting on overloaded servers will always struggle with TTFB. A VPS with adequate CPU and memory gives you control and better baseline performance.

How do you measure the impact?

After applying these fixes, measure the results. Don't rely on gut feel.

Use PageSpeed Insights or Lighthouse in Chrome DevTools to get lab scores. Run tests three times and average the results; scores fluctuate.

Check Search Console's Core Web Vitals report for real-world field data. Field data reflects actual user experience across different devices and network conditions. It's slower to update—Google collects data over 28 days—but it's what matters for ranking.

Compare before-and-after metrics for LCP, FID (or INP, which is replacing FID), and CLS. A 50% LCP improvement is realistic if you started with unoptimized images and render-blocking resources.

Monitor ongoing performance with tools like SpeedCurve or Calibre if you manage multiple sites or deploy frequently. Automated alerts catch regressions before they hurt traffic.

Start with images and JavaScript

Core Web Vitals optimization isn't a one-time checklist. You apply these fixes, measure, and iterate. The biggest wins come from image optimization and JavaScript splitting because those two categories dominate page weight and execution time. Fix those first, then move to fonts, third-party scripts, and server response. Test real user metrics in Search Console, not just lab scores. The gap between lab and field data tells you whether your optimizations actually help your audience.

Most hosting-related performance issues trace back to unoptimized assets, missing cache headers, or slow database queries. Address those and you'll clear the Core Web Vitals thresholds without exotic tooling.

FAQ

What's the fastest way to improve LCP?

Optimize your hero image first: convert it to WebP, resize it to the exact display size, and serve it from a CDN. That single change often cuts LCP in half.

Does lazy loading improve Core Web Vitals?

Yes, for CLS and overall page weight, but never lazy-load your LCP element. Lazy-loading above-the-fold images delays LCP and hurts your score.

Should I use a plugin or do this manually?

Plugins like WP Rocket or NitroPack handle most optimizations automatically on WordPress and work well for non-technical users. Manual configuration gives you more control and avoids plugin overhead if you're comfortable editing config files.

How often should I test Core Web Vitals?

Test after every significant change—new theme, plugin update, content migration. Set up monthly checks otherwise to catch gradual performance drift.