What Core Web Vitals Actually Measure
Google's three Core Web Vitals each target a different dimension of perceived performance:
- LCP (Largest Contentful Paint) — how fast the main content loads. Target: under 2.5 s.
- CLS (Cumulative Layout Shift) — how much content jumps around while loading. Target: under 0.1.
- INP (Interaction to Next Paint) — how fast the page responds to clicks and taps. Target: under 200 ms.
The worst part about WordPress defaults: themes load full jQuery, page builders inject dozens of render-blocking scripts, and images are rarely lazy-loaded or properly sized. All three vitals suffer out of the box.
Fixing LCP — Largest Contentful Paint
The LCP element is almost always the hero image or the H1. Slow LCP usually means one of three things: a slow server, a render-blocking resource, or an unoptimised image.
1. Preload the LCP image
If your hero image is known at render time, tell the browser to start fetching it immediately with a preload hint:
<link
rel="preload"
as="image"
href="/wp-content/uploads/hero.webp"
imagesrcset="/wp-content/uploads/hero-400.webp 400w,
/wp-content/uploads/hero-800.webp 800w,
/wp-content/uploads/hero-1200.webp 1200w"
imagesizes="100vw"
/>
Add this to header.php or via wp_head for dynamic pages. This alone can cut LCP by 400–800 ms on cold loads.
2. Serve images in WebP
WordPress 5.8+ generates WebP automatically. Make sure your theme requests the right size and format, and avoid using full-size images in thumbnails:
<?php
// Always use wp_get_attachment_image() — never hardcode img tags for uploads
echo wp_get_attachment_image( $attachment_id, 'large', false, [
'loading' => 'eager', // LCP image — don't lazy-load it
'fetchpriority' => 'high',
'class' => 'hero-image',
] );
3. Eliminate render-blocking resources
Audit with Chrome DevTools > Performance > Network waterfall. Any CSS or JS that blocks the first render delays LCP. Add defer to non-critical scripts and inline critical CSS:
add_filter( 'script_loader_tag', function( $tag, $handle ) {
$defer = [ 'my-slider', 'my-analytics' ];
if ( in_array( $handle, $defer, true ) ) {
return str_replace( '<script ', '<script defer ', $tag );
}
return $tag;
}, 10, 2 );
Fixing CLS — Cumulative Layout Shift
Layout shifts happen when the browser doesn't know an element's size before it loads. The fix is always the same: give every element that loads asynchronously a reserved size.
Images without dimensions
This is the #1 CLS culprit in WordPress. Always set width and height attributes on images — even in CSS-fluid layouts. The browser uses them to calculate aspect ratio before the image loads:
<img src="photo.webp" width="1200" height="800" alt="..."
style="width:100%; height:auto;" />
Web fonts
Fonts are a subtle CLS source. Use font-display: optional for body text if you can live with a system font fallback, or font-display: swap combined with a size-adjusted fallback to minimise shift:
@font-face {
font-family: 'Inter';
src: url('/fonts/inter.woff2') format('woff2');
font-display: swap;
size-adjust: 100.3%; /* tune to match fallback metrics */
ascent-override: 90%;
}
Ads and embeds
Reserve space for ads and iframes with a fixed container before the content loads. A wrapper with a known aspect-ratio prevents the page from reflowing when the embed renders.
Fixing INP — Interaction to Next Paint
INP replaced FID in 2024 and is harder to fix because it measures all interactions, not just the first. Poor INP in WordPress is almost always caused by too much JavaScript running on the main thread.
Audit your JS payload
Open Chrome DevTools > Coverage. Everything in red is unused JavaScript. Common culprits on WordPress sites:
- jQuery loaded globally but only used by one widget
- Page builder scripts loaded site-wide instead of per-template
- Full Google Analytics bundle when GA4 Measurement Protocol would suffice
Conditionally load scripts
add_action( 'wp_enqueue_scripts', function() {
// Only load slider JS on pages that use it
if ( is_page_template( 'template-home.php' ) ) {
wp_enqueue_script( 'my-slider', get_template_directory_uri() . '/assets/js/slider.js', [], '1.0', true );
}
} );
Defer third-party scripts
Chat widgets, Intercom, Zendesk — load them after the user's first interaction with a facade pattern:
<div id="chat-facade" style="cursor:pointer" aria-label="Open chat">
💬 Chat with us
</div>
<script>
document.getElementById('chat-facade').addEventListener('click', function() {
var s = document.createElement('script');
s.src = 'https://cdn.chatwidget.com/widget.js';
document.head.appendChild(s);
this.remove();
}, { once: true });
</script>
Server-Level Wins
Client-side optimisations have a ceiling. If your server TTFB (Time to First Byte) is over 600 ms, you cannot get a good LCP score regardless of frontend work.
- Enable full-page caching — WP Rocket, W3 Total Cache, or server-level Nginx FastCGI cache. This is the single biggest win on most sites.
- Use a CDN — Cloudflare's free tier eliminates most static asset latency globally.
- Enable gzip/Brotli compression on your web server.
- Optimise your database — autoloaded options table is a common silent slow-down. Query Monitor plugin exposes this.
Measuring and Iterating
Never optimise by feel. Use these tools in order:
- PageSpeed Insights — real-user data (CrUX) plus lab data. Use this as ground truth.
- Chrome DevTools > Performance — diagnose specific bottlenecks in the waterfall.
- WebPageTest — filmstrip view and connection throttling for deep analysis.
- Search Console > Core Web Vitals report — shows field data per URL group, which is what Google actually uses for ranking.