Core Web Vitals matter because Google ranks on them, but more importantly because users abandon slow sites. I've watched three metrics—Largest Contentful Paint, Cumulative Layout Shift, and Interaction to Next Paint—move from red to green on production sites by applying fixes that don't require a full rebuild. Some took ten minutes. Others needed an afternoon and a CDN config change.
This is what worked.
Fix 1: Preconnect to critical third-party origins
Every font, analytics script, or payment widget hosted elsewhere costs you a DNS lookup, TCP handshake, and TLS negotiation before the first byte arrives. Preconnect hints tell the browser to open those connections early, shaving 200-600 ms off LCP when your hero image or web font lives on a CDN.
Add this to your document <head>:
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link rel="preconnect" href="https://cdn.yoursite.com">
On a WordPress site pulling fonts from Google, LCP dropped 340 ms because the browser started the connection while parsing HTML instead of waiting until the CSS requested the font. Limit preconnect to two or three origins—the browser won't parallelize twenty of them, and you'll waste the hint.
Fix 2: Self-host Google Fonts
Google Fonts is convenient but adds two render-blocking requests: one to fonts.googleapis.com for the CSS, another to fonts.gstatic.com for the WOFF2 file. Self-hosting cuts that to zero external requests.
Download the font files, add them to your theme or public directory, and declare them in your CSS:
@font-face {
font-family: 'Inter';
font-style: normal;
font-weight: 400;
font-display: swap;
src: url('/fonts/inter-v12-latin-regular.woff2') format('woff2');
}
Set font-display: swap so text renders immediately in a fallback font, then swaps when the custom font loads. On a React marketing site, self-hosting Inter with swap improved LCP by 280 ms and eliminated a CLS spike caused by late font arrival.
Fix 3: Add width and height to every image and iframe
Browsers reserve space for media only if you tell them the dimensions. Without explicit width and height attributes, the layout shifts when the image loads and the browser discovers its actual size. That's your CLS score.
For an <img> tag:
<img src="hero.jpg" alt="Dashboard" width="1200" height="675" loading="lazy">
The browser uses the aspect ratio even if CSS later resizes the image to 100% width. WordPress themes from 2019 and earlier often skip dimensions; add them in the template or use a plugin that injects them automatically. Iframes need dimensions too:
<iframe src="https://www.youtube.com/embed/xyz" width="560" height="315" loading="lazy"></iframe>
After adding dimensions to thirty images on a WordPress blog, CLS dropped from 0.18 to 0.04. The layout stopped jumping.
Fix 4: Load non-critical CSS asynchronously
Render-blocking CSS stops the browser from painting anything until the entire stylesheet downloads and parses. If your CSS bundle includes styles for a contact form below the fold, you're blocking the hero section for no reason.
Split critical above-the-fold CSS into a separate file and inline it, or load the full stylesheet with media="print" then swap it:
<link rel="stylesheet" href="/css/main.css" media="print" onload="this.media='all'">
<noscript><link rel="stylesheet" href="/css/main.css"></noscript>
The browser loads the CSS without blocking render, then applies it once it arrives. On a WordPress site with a 180 KB stylesheet, this cut LCP by 520 ms because the hero image started rendering before the entire CSS file finished.
Fix 5: Lazy-load images and iframes below the fold
The browser doesn't know which images are visible on load, so it fetches all of them by default. That wastes bandwidth and delays LCP by queuing dozens of requests when you only need three.
Add loading="lazy" to any image or iframe outside the viewport:
<img src="screenshot.png" alt="Example" width="800" height="600" loading="lazy">
Never lazy-load your LCP image. If your hero banner has loading="lazy", the browser won't start fetching it until layout finishes, destroying your LCP score. On a product landing page, lazy-loading eight below-the-fold images freed up bandwidth for the hero, improving LCP by 190 ms.
Fix 6: Serve images in next-gen formats with srcset
JPEG and PNG are 30-50% larger than WebP or AVIF at equivalent quality. Serving modern formats to browsers that support them cuts transfer time, especially on mobile.
Use the <picture> element to offer WebP with a JPEG fallback:
<picture>
<source srcset="hero.webp" type="image/webp">
<img src="hero.jpg" alt="Hero" width="1200" height="675">
</picture>
Add srcset for responsive images so mobile devices download a 400 px version instead of the 2400 px desktop file:
<img src="hero-800.jpg"
srcset="hero-400.jpg 400w, hero-800.jpg 800w, hero-1200.jpg 1200w"
sizes="(max-width: 600px) 400px, 800px"
alt="Hero" width="1200" height="675">
After converting a WordPress gallery to WebP and adding srcset, total image weight dropped from 2.1 MB to 780 KB, and LCP improved by 610 ms on a 4G connection.
Fix 7: Reduce and defer JavaScript execution
Heavy JavaScript blocks the main thread, delaying INP and making the page feel sluggish. Every analytics tag, chat widget, and social sharing button adds script that fights for CPU time during interaction.
Defer non-critical scripts:
<script src="/js/analytics.js" defer></script>
Better yet, delay third-party scripts until user interaction:
let scriptsLoaded = false;
document.addEventListener('scroll', function loadScripts() {
if (!scriptsLoaded) {
const script = document.createElement('script');
script.src = 'https://cdn.thirdparty.com/widget.js';
document.body.appendChild(script);
scriptsLoaded = true;
}
document.removeEventListener('scroll', loadScripts);
}, { once: true });
On a React app with five third-party tags, deferring everything except the payment provider reduced Total Blocking Time by 1,200 ms and brought INP under 200 ms.
Fix 8: Enable compression and HTTP/2 on your origin
Text assets—HTML, CSS, JavaScript, JSON—compress by 60-80% with Brotli or Gzip. If your server isn't compressing responses, every page load transfers three times the bytes it should.
For Apache, enable mod_deflate or mod_brotli in your .htaccess:
<IfModule mod_deflate.c>
AddOutputFilterByType DEFLATE text/html text/css application/javascript application/json
</IfModule>
For Nginx:
gzip on;
gzip_types text/css application/javascript application/json;
gzip_min_length 1024;
HTTP/2 multiplexes requests over a single connection instead of opening six separate TCP streams, cutting latency when the browser fetches twenty assets. Most hosting panels (cPanel, Plesk) enable HTTP/2 by default if you have an SSL certificate. Check with:
curl -I --http2 https://yoursite.com
Look for HTTP/2 200 in the response. Enabling Brotli and HTTP/2 on a VPS reduced page load time by 1.8 seconds on a 3G connection.
Fix 9: Set explicit cache headers for static assets
Browsers cache files only if you tell them to. Without cache headers, every visit re-downloads the same logo, stylesheet, and JavaScript bundle.
Set long expiry for versioned assets in Apache:
<IfModule mod_expires.c>
ExpiresActive On
ExpiresByType image/jpeg "access plus 1 year"
ExpiresByType text/css "access plus 1 year"
ExpiresByType application/javascript "access plus 1 year"
</IfModule>
In Nginx:
location ~* \.(jpg|jpeg|png|css|js|woff2)$ {
expires 1y;
add_header Cache-Control "public, immutable";
}
Returning visitors hit the cache instead of the network, dropping LCP to under 800 ms on repeat views.
Fix 10: Use a CDN with automatic image optimization
CDNs cache content close to users, cutting latency from 200 ms (cross-continent) to 20 ms (same region). Many CDNs also resize and convert images on the fly, so you upload one high-res JPEG and the CDN serves a 400 px WebP to mobile browsers.
Cloudflare, BunnyCDN, and Cloudinary all offer this. After moving a WordPress site to Cloudflare with Polish (automatic image optimization) enabled, LCP dropped 40% for users outside North America and CLS stopped entirely because images arrived with correct dimensions.
Fix 11: Eliminate layout shifts from dynamic content
Ads, cookie banners, and lazy-loaded sections cause CLS when they inject into the page without reserved space. Reserve a min-height before the content arrives.
For an ad slot:
<div class="ad-container" style="min-height: 250px;">
<!-- Ad script loads here -->
</div>
For a React component that fetches data:
function ProfileCard() {
const [data, setData] = useState(null);
if (!data) {
return <div style={{ minHeight: '200px' }}>Loading...</div>;
}
return <div>{data.name}</div>;
}
The placeholder holds space so the layout doesn't jump when content arrives. On a news site with programmatic ads, setting min-height on ad slots reduced CLS from 0.22 to 0.06.
What you should test first
Run your site through PageSpeed Insights or Chrome DevTools to see which metric is worst. LCP over 2.5 seconds? Start with images and third-party requests. CLS above 0.1? Check image dimensions and reserve space for dynamic content. INP over 200 ms? Audit JavaScript execution and defer what you can.
Fixes stack. Self-hosting fonts + preconnect + WebP conversion might each save 200 ms, but together they bring LCP under the 2.5 second threshold. You don't need all eleven—apply the three that address your worst metric, measure, and iterate.
How do I know which image is my LCP element?
Open Chrome DevTools, go to the Performance tab, record a page load, and look for the LCP marker in the timeline. DevTools highlights the element. If it's an image, optimize that one first—make it smaller, convert to WebP, add preload if necessary.
Does lazy-loading hurt SEO?
No. Googlebot renders JavaScript and respects loading="lazy". Just don't lazy-load your LCP image or any content critical for understanding the page. Below-the-fold images are safe.
Can I fix Core Web Vitals on shared hosting?
Yes, but server response time matters. If your shared host is slow, enabling compression and caching helps, but you might still need a CDN to hit green scores. Self-hosting fonts, adding image dimensions, and deferring scripts work on any host.
Should I inline all my CSS?
No. Inline only the critical above-the-fold CSS—typically 10-15 KB. Inlining everything bloats your HTML and prevents the browser from caching the stylesheet. Load the rest asynchronously or defer it.
Where to focus next
Measure before and after every change. Core Web Vitals scores come from real user data (CrUX) over 28 days, so a fix won't show up in Search Console immediately. Use PageSpeed Insights lab data and Chrome DevTools to validate improvements locally, then wait for field data to update.
If you're still in the orange after these fixes, profile your JavaScript with Chrome DevTools Coverage tab to find unused code, audit third-party scripts, and consider server-side rendering or static generation for React apps. Most sites hit green with five to seven of these fixes applied.
