Core Web Vitals directly affect how Google ranks your site, and more importantly, whether visitors stick around or bounce. The three metrics—Largest Contentful Paint (LCP), Cumulative Layout Shift (CLS), and Interaction to Next Paint (INP)—measure loading speed, visual stability, and responsiveness. I've walked dozens of clients through the same fixes, and a handful of server-side and front-end changes reliably move scores from red to green.
1. Compress and serve next-gen images
Heavy images kill LCP. A 2 MB hero image loads slowly even on fast connections, and mobile users on patchy networks wait forever.
Convert JPEGs and PNGs to WebP or AVIF. WebP typically cuts file size by 25–35% with no visible quality loss. AVIF goes further but has spottier browser support in older clients—check your audience analytics first. For WordPress, install a plugin like ShortPixel or Imagify that converts on upload and serves WebP with a JPEG fallback. For static sites, use a build-time tool like imagemin or squoosh-cli.
Set explicit width and height attributes on every <img> tag. Browsers reserve the correct space before the image loads, which prevents layout shift. If you're using responsive images with srcset, calculate the aspect ratio and apply it via CSS:
img {
aspect-ratio: 16 / 9;
width: 100%;
height: auto;
}
For photos served from your origin, enable GZIP or Brotli compression in your web server config. In Apache, add to .htaccess:
<IfModule mod_deflate.c>
AddOutputFilterByType DEFLATE image/svg+xml
</IfModule>
<IfModule mod_brotli.c>
AddOutputFilterByType BROTLI_COMPRESS image/svg+xml
</IfModule>
In Nginx:
gzip on;
gzip_types image/svg+xml;
brotli on;
brotli_types image/svg+xml;
SVGs benefit from compression; raster formats like WebP are already compressed and won't shrink further.
2. Lazy-load offscreen images and iframes
Browsers waste bandwidth downloading images the user hasn't scrolled to yet. Native lazy loading defers those requests until the element enters the viewport.
Add loading="lazy" to any image below the fold:
<img src="photo.webp" alt="Description" width="800" height="600" loading="lazy">
Do the same for iframes (YouTube embeds, Google Maps):
<iframe src="https://www.youtube.com/embed/VIDEO_ID" loading="lazy"></iframe>
Leave hero images and anything visible on initial page load as loading="eager" or omit the attribute—lazy-loading above-the-fold content delays LCP. In WordPress, most modern themes and page builders add loading="lazy" automatically, but double-check your hero image markup.
For fine-grained control, use the Intersection Observer API to load images only when they're 200 px from entering the viewport. This gives you a head start over the native threshold.
3. Eliminate render-blocking CSS
Every stylesheet in <head> blocks rendering until it downloads and parses. The browser won't paint anything until all CSS is ready.
Inline critical CSS—the styles needed to render above-the-fold content—directly in the <head>. Extract it with a tool like Critical or Penthouse, then paste the result into a <style> block. Load the full stylesheet asynchronously:
<style>
/* Critical CSS here */
body { margin: 0; font-family: sans-serif; }
.hero { background: #000; color: #fff; padding: 2rem; }
</style>
<link rel="preload" href="/css/main.css" as="style" onload="this.onload=null;this.rel='stylesheet'">
<noscript><link rel="stylesheet" href="/css/main.css"></noscript>
The preload + onload trick loads the stylesheet without blocking. For WordPress, plugins like WP Rocket and Autoptimize handle critical CSS extraction, though results vary by theme complexity.
Remove unused CSS. Run a coverage report in Chrome DevTools (Ctrl+Shift+P → "Show Coverage") to see how much of each stylesheet actually applies to the current page. Tools like PurgeCSS strip unused rules from Tailwind, Bootstrap, and other frameworks. In a build pipeline:
npx purgecss --css styles.css --content index.html --output dist/
For third-party stylesheets (Google Fonts, icon libraries), combine requests. Instead of three separate font files, load one with multiple weights in a single request:
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;600;700&display=swap" rel="stylesheet">
The display=swap parameter shows fallback text immediately and swaps in the web font when ready, which prevents invisible text (FOIT).
4. Defer and minimize JavaScript
JavaScript is the biggest INP offender. Long tasks lock the main thread, and the browser can't respond to clicks or taps until the script finishes.
Add defer to every non-critical script:
<script src="app.js" defer></script>
Deferred scripts download in parallel with HTML parsing but execute only after the DOM is ready. For analytics, chat widgets, and other non-essential scripts, use async instead—they run as soon as they download, without waiting for DOM ready.
Split large bundles by route or feature. Ship only the code needed for the current page. With Webpack:
module.exports = {
optimization: {
splitChunks: {
chunks: 'all',
},
},
};
This extracts vendor libraries and shared modules into separate files that the browser caches across pages.
Remove or replace heavy libraries. Moment.js is 70 KB minified; date-fns or the native Intl.DateTimeFormat API does the same work in a fraction of the size. Lodash is useful, but importing the entire library for two helper functions is wasteful—import individual methods:
import debounce from 'lodash/debounce';
import throttle from 'lodash/throttle';
For WordPress, open the Lighthouse report and look for render-blocking JavaScript from plugins. Disable any plugin that loads scripts on every page when it's only needed on one (contact forms, sliders, e-commerce widgets). Most caching plugins let you exclude scripts from specific pages.
5. Tune your CDN for Core Web Vitals
A CDN cuts latency by serving assets from edge locations close to users, but default settings often leave performance on the table.
Enable HTTP/3 and QUIC if your CDN supports it—Cloudflare, Fastly, and BunnyCDN all do. HTTP/3 reduces connection setup time and handles packet loss better than HTTP/2, which helps on mobile networks. In Cloudflare, it's under Network → enable HTTP/3.
Set long cache headers for static assets (images, CSS, JS). Twelve months is standard:
location ~* \.(jpg|jpeg|png|webp|css|js|woff2)$ {
expires 1y;
add_header Cache-Control "public, immutable";
}
The immutable directive tells the browser not to revalidate the file even on reload, which saves round trips.
Use Cache-Control: s-maxage to set a separate, longer TTL at the CDN while keeping browser cache shorter. This lets you purge stale content from the CDN without forcing every visitor to re-download:
Cache-Control: public, max-age=3600, s-maxage=31536000
Browser caches for one hour; CDN caches for a year.
Enable Brotli compression at the CDN edge. Cloudflare does this automatically for text-based assets; other CDNs may need a toggle. Brotli typically beats GZIP by 15–20% on compression ratio.
Prefetch DNS and preconnect to third-party domains you can't avoid (analytics, ads, payment gateways). Add to <head>:
<link rel="dns-prefetch" href="https://www.google-analytics.com">
<link rel="preconnect" href="https://checkout.stripe.com" crossorigin>
This resolves DNS and establishes a connection before the browser actually requests a resource, shaving 100–300 ms off the first request to that origin.
6. Reserve space for ads and embeds
CLS spikes when ads, social widgets, or dynamic content loads after the initial paint and shoves everything down the page. The user taps a button, the ad loads, and they accidentally click the ad instead.
Wrap ad slots and embeds in a container with a fixed aspect ratio. For a 300×250 ad:
<div style="width: 300px; height: 250px;">
<!-- Ad code here -->
</div>
If the ad unit can vary in size, use min-height to reserve the tallest possible height.
For YouTube or Vimeo embeds, wrap the iframe in a responsive container:
<div style="position: relative; padding-bottom: 56.25%; height: 0;">
<iframe src="https://www.youtube.com/embed/VIDEO_ID"
style="position: absolute; top: 0; left: 0; width: 100%; height: 100%;"
loading="lazy"></iframe>
</div>
The padding-bottom: 56.25% creates a 16:9 aspect ratio box; the iframe fills it. No layout shift.
Font loading causes shift too. Use font-display: swap (covered earlier) and match the fallback font's metrics to the web font as closely as possible. Tools like Fallback Font Generator calculate the right adjustments.
7. Upgrade server response time (TTFB)
Slow Time to First Byte kills every downstream metric. If the server takes two seconds to respond, nothing else matters—LCP will be terrible.
Enable an opcode cache for PHP. Most shared hosts run OPcache by default, but VPS owners sometimes forget to enable it. Check with:
php -i | grep opcache
If it's missing, install and enable it in php.ini:
zend_extension=opcache.so
opcache.enable=1
opcache.memory_consumption=128
opcache.interned_strings_buffer=8
opcache.max_accelerated_files=10000
opcache.revalidate_freq=60
Restart PHP-FPM or Apache.
For WordPress, install Redis or Memcached as an object cache. This caches database query results in memory, which cuts response time by 50–70% on database-heavy pages. Most managed WordPress hosts offer Redis as a one-click option. On a VPS, install Redis:
sudo apt install redis-server
sudo systemctl enable redis-server
Then install the Redis Object Cache plugin in WordPress and enable it.
Use a full-page cache. For static sites, this is automatic—files are already HTML. For WordPress, enable caching in WP Rocket, W3 Total Cache, or your host's built-in cache. This stores entire rendered pages and serves them without touching PHP or the database. First visitor gets a slow load; everyone else gets instant HTML.
Check your database for bloat. Run a query to count post revisions:
SELECT COUNT(*) FROM wp_posts WHERE post_type = 'revision';
If you see thousands, delete them. Use WP-Optimize or run:
DELETE FROM wp_posts WHERE post_type = 'revision';
Then optimize tables:
OPTIMIZE TABLE wp_posts, wp_postmeta, wp_options;
What should you measure first?
Run a Lighthouse audit in Chrome DevTools (F12 → Lighthouse tab → Analyze page load). Focus on the three Core Web Vitals metrics in the report. LCP above 2.5 seconds is poor; under 2.5 is good. CLS above 0.1 is poor; under 0.1 is good. INP above 200 ms is poor; under 200 is good.
Test on a throttled connection (Lighthouse defaults to simulated slow 4G) and on a real mobile device. Desktop scores often look fine while mobile is red across the board.
Use PageSpeed Insights for field data—real measurements from Chrome users who visited your site. Lab data (Lighthouse) shows what's possible under controlled conditions; field data shows what actual users experience. If field data is poor but lab data is good, the problem is usually high traffic, server overload, or third-party scripts that don't appear in a clean test environment.
Monitor Core Web Vitals in Search Console under Experience → Core Web Vitals. Google groups URLs by status (Good, Needs Improvement, Poor) and shows which pages need attention. Fix the worst offenders first—usually the homepage and top landing pages.
How much does CDN help Core Web Vitals?
A CDN alone won't fix a 10-second LCP, but it typically cuts 200–500 ms off TTFB and asset load times by serving from nearby edge servers. The gain is larger for geographically distributed traffic.
Can I just use a plugin for WordPress?
Plugins like WP Rocket and Perfmatters automate lazy loading, CSS/JS optimization, and caching, which handles 70% of Core Web Vitals fixes. But heavy themes, page builders, and third-party scripts still need manual cleanup.
What if Lighthouse scores are good but Search Console shows poor?
Field data reflects real users with slow connections, old devices, and ad blockers that break lazy loading. Test on throttled mobile, check for server overload during peak hours, and audit third-party scripts.
Does server location matter in 2026?
Yes, if you're not using a CDN. Without a CDN, a server in the US serves slow responses to users in Asia or Europe. With a CDN, origin location matters less—assets cache at the edge.
Should I defer Google Analytics?
Yes. Analytics doesn't need to run before the page renders. Load it with async or defer, or use a tag manager that loads scripts after user interaction.
Run the fixes in order of impact
Start with images and lazy loading—they're quick wins that improve LCP and CLS in one pass. Then tackle render-blocking CSS and defer JavaScript. CDN and server tuning come last because they require hosting changes or command-line access. Each fix stacks; a site that does all seven reliably scores green in Lighthouse and climbs in search rankings. I've seen LCP drop from 4.2 seconds to 1.8 and CLS fall from 0.3 to 0.02 with these exact steps, no expensive re-platform required.
