Google uses three Core Web Vitals metrics to score page experience: Largest Contentful Paint (LCP), First Input Delay (FID), and Cumulative Layout Shift (CLS). Sites that fail these thresholds lose ranking visibility and conversions. The good news is most failures trace back to a handful of server misconfigurations and fixable code patterns.
I've walked dozens of clients through these same optimizations in support tickets. The biggest gains usually come from image handling and JavaScript execution order, not from expensive CDN upgrades or complete rewrites. You can fix the majority of issues in an afternoon if you know where to look.
Understanding the three metrics
LCP measures how long your largest above-the-fold element takes to render. Google wants this under 2.5 seconds. Common culprits: unoptimized hero images, render-blocking CSS, and slow server response times.
FID tracks the delay between a user's first click and the browser's response. The threshold is 100 milliseconds. Long-running JavaScript that blocks the main thread causes FID spikes, especially on mobile devices with slower CPUs.
CLS quantifies unexpected layout shifts—text jumping when a font loads, images pushing content down because height attributes are missing. Google's threshold is 0.1 or lower. Sites with ads, embeds, and dynamically injected content often struggle here.
All three metrics come from real-user data in Chrome, collected through the Chrome User Experience Report. Google Search Console surfaces your field data in the Core Web Vitals report. If you don't have 28 days of traffic yet, use Lighthouse in Chrome DevTools for lab estimates, but remember field data is what counts for rankings.
Fix 1: Serve next-gen image formats with explicit dimensions
Most LCP failures I see involve a massive JPEG hero image served at full resolution. Browsers have to download, decode, and paint it before the page feels loaded.
Convert your hero and above-the-fold images to WebP or AVIF. These formats cut file size by 30-50% with no visible quality loss. Use the picture element with fallback sources:
<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>
Notice the width and height attributes on the img tag. The browser reserves space before the image loads, preventing CLS. Measure your images and hard-code dimensions. Do this for every image in the viewport.
If you're on WordPress, install a plugin that generates WebP on upload and serves them automatically—ShortPixel and Imagify both work. Check that your hosting plan allows you to serve WebP; older Apache configs sometimes strip the MIME type. Add this to .htaccess if needed:
<IfModule mod_mime.c>
AddType image/webp .webp
</IfModule>
For cPanel users, you can also set the MIME type in the MIME Types interface under Advanced.
Fix 2: Preload critical resources and preconnect to third-party origins
Your browser discovers CSS, fonts, and JavaScript as it parses HTML. By the time it finds a web font buried in a stylesheet, hundreds of milliseconds have passed. Preload hints tell the browser to fetch critical assets immediately.
Add these tags in your <head>, before any other stylesheets:
<link rel="preload" href="/fonts/inter-var.woff2" as="font" type="font/woff2" crossorigin>
<link rel="preload" href="/css/critical.css" as="style">
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
Preload one or two hero fonts. Don't preload every font file or you'll waste bandwidth. Self-host your fonts whenever possible to avoid the DNS and TLS handshake latency of third-party domains.
If you must load fonts from Google Fonts or Adobe Fonts, use preconnect so the browser opens the connection early. This typically saves 100-300 milliseconds on mobile.
Fix 3: Defer non-critical JavaScript and split bundles
JavaScript blocks HTML parsing by default. Every synchronous script tag stalls the browser until the file downloads and executes. If your site loads jQuery, analytics, chat widgets, and a marketing automation script in the <head>, you're adding seconds to LCP and killing FID.
Move all non-essential scripts to the bottom of the body with the defer attribute:
<script src="/js/analytics.js" defer></script>
<script src="/js/chat-widget.js" defer></script>
Deferred scripts download in parallel but execute in order after HTML parsing finishes. For scripts that don't depend on DOM content or each other, use async instead:
<script src="/js/tracking.js" async></script>
If your JavaScript bundle is over 200 KB, split it. Build tools like Webpack and Vite support code splitting out of the box. Ship a small initial bundle that renders the page, then lazy-load the rest:
// Load heavy component only when needed
button.addEventListener('click', async () => {
const module = await import('./heavy-component.js');
module.render();
});
For WordPress, WP Rocket and Autoptimize can defer scripts automatically, but test carefully—some themes and plugins break when scripts load out of order.
Fix 4: Enable server-side caching and set proper cache headers
Slow server response time (TTFB) directly impacts LCP. If your HTML takes 800 milliseconds to arrive, you've already burned a third of your LCP budget before a single byte of CSS or images loads.
Turn on full-page caching. In cPanel hosting, this usually means enabling LiteSpeed Cache if your server runs LiteSpeed, or setting up Redis object caching with a plugin if you're on Apache with WordPress. Cached pages should return in under 200 milliseconds.
Set long cache lifetimes for static assets. Add this to your .htaccess or your Nginx config:
<IfModule mod_expires.c>
ExpiresActive On
ExpiresByType image/webp "access plus 1 year"
ExpiresByType image/jpeg "access plus 1 year"
ExpiresByType image/png "access plus 1 year"
ExpiresByType text/css "access plus 1 year"
ExpiresByType application/javascript "access plus 1 year"
ExpiresByType font/woff2 "access plus 1 year"
</IfModule>
For Nginx:
location ~* \.(jpg|jpeg|png|webp|css|js|woff2)$ {
expires 1y;
add_header Cache-Control "public, immutable";
}
Use cache busting (filename hashing) so you can update assets without waiting for browser caches to expire. Most build tools do this automatically.
If you're on shared hosting and TTFB is still slow, check your database query performance. Install Query Monitor on WordPress to find slow queries, then add indexes or cache the results.
Fix 5: Reserve space for ads, embeds, and dynamic content
CLS happens when content shifts after the page loads. The worst offenders are ads, YouTube embeds, and social media widgets that inject unknown-height content into the DOM.
For ads, wrap each ad slot in a container with a fixed aspect ratio using CSS:
.ad-container {
position: relative;
width: 100%;
padding-bottom: 56.25%; /* 16:9 aspect ratio */
}
.ad-container > * {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
}
This reserves space before the ad loads. Measure your typical ad dimensions and adjust the padding percentage accordingly.
For YouTube embeds, use the same technique or switch to the lite-youtube-embed web component, which lazy-loads the iframe and eliminates CLS entirely.
Never inject content above existing content after page load. If you must load a banner or notification dynamically, push existing content down smoothly with a CSS transition, or place the banner in a reserved fixed-height container.
Fix 6: Inline critical CSS and eliminate render-blocking stylesheets
External stylesheets block rendering. The browser downloads your main CSS file, parses it, builds the CSSOM, then paints. If that stylesheet is 150 KB and includes styles for every page on your site, you're wasting time.
Inline the CSS needed to render above-the-fold content directly in the <head>:
<style>
/* Critical CSS for hero, header, and first viewport */
body { margin: 0; font-family: Inter, sans-serif; }
.hero { display: flex; min-height: 60vh; background: #f8f9fa; }
/* ...more critical rules */
</style>
Then load the full stylesheet asynchronously:
<link rel="preload" href="/css/full.css" as="style" onload="this.onload=null;this.rel='stylesheet'">
<noscript><link rel="stylesheet" href="/css/full.css"></noscript>
Tools like Critical or Penthouse can extract critical CSS automatically by rendering your page in a headless browser. For WordPress, plugins like WP Rocket and FlyingPress offer critical CSS generation with varying reliability—test the output to make sure nothing breaks.
Keep the inlined CSS under 14 KB so it fits in the first TCP round-trip. Compress it with cssnano or clean-css.
So how do you verify the fixes worked?
Run Lighthouse in Chrome DevTools after every change. Open DevTools, go to the Lighthouse tab, select "Performance" and "Mobile", then click "Analyze page load". Look at the LCP, FID (or the new Interaction to Next Paint metric), and CLS scores.
Lighthouse gives you lab data, which is useful for debugging but doesn't reflect real-world conditions. Check Google Search Console's Core Web Vitals report for field data after changes have been live for a few days. If your site doesn't get enough Chrome traffic, use PageSpeed Insights to see CrUX data for your origin.
Test on a throttled mobile connection. Desktop performance often looks great while mobile users still struggle. In Chrome DevTools, open the Network tab and set throttling to "Slow 3G". Your LCP should still hit the threshold.
Monitor your Web Vitals over time with Real User Monitoring. Tools like SpeedCurve, Calibre, or the free web-vitals JavaScript library let you track scores in production and catch regressions before Google does.
What to test first
Start with images. Check that every image in the first viewport has explicit width and height attributes and is served in a modern format. That fix alone often pushes LCP under the threshold.
Next, audit your JavaScript. Defer everything that isn't needed for initial render and split large bundles. This improves both LCP and FID with minimal risk.
Finally, measure and reserve space for ads and embeds to eliminate CLS. Use the aspect-ratio CSS property or padding-bottom trick to lock dimensions before content loads.
Run Lighthouse after each fix and watch the metrics shift. Field data will catch up in a few days, and you'll see the improvements reflected in Search Console. Google prioritizes sites that pass all three Core Web Vitals, so these fixes directly impact your search visibility and conversion rates.
