Skip to content
ExponaBLOG
WORDPRESS DEVELOPMENT & PLUGINS · DAY 038 / 100

Core Web Vitals for WordPress: best practices explained

Improve LCP, INP and CLS.

Expona Admin8 min read

Introduction

Core Web Vitals for WordPress remain one of the most overlooked optimization levers on production sites. We've watched teams ship beautifully crafted WordPress themes and plugins, only to discover their Lighthouse scores hover in the orange zone because of unoptimized hero images, render-blocking assets, or third-party tracking overhead. In this article, we'll walk through concrete strategies to improve LCP, INP, and CLS—the three metrics that matter most for both user experience and search ranking.

The good news: most of these improvements don't require a rebuild. You can implement them incrementally, measure progress with real user monitoring (RUM), and see measurable gains within days.

Preload the Hero Image

Largest Contentful Paint (LCP) is almost always dominated by your above-the-fold image. On an e-commerce site we worked on, a full-width hero JPG was consistently rendering at 3.8 seconds. The fix was simple: preload the image resource in the document head.

When browsers encounter an image in your HTML, they parse the DOM first, build the render tree, then fetch the asset. Preload tells the browser to start fetching the image as soon as possible, in parallel with parsing, without waiting for the CSS or layout phase.

Add this to your WordPress theme header:

php
<?php
// In functions.php or a custom plugin
add_action( 'wp_head', function() {
    $hero_image = get_theme_mod( 'hero_image_url' );
    if ( $hero_image ) {
        echo sprintf(
            '<link rel="preload" as="image" href="%s" imagesrcset="%s" imagesizes="100vw">',
            esc_url( $hero_image ),
            esc_attr( wp_get_attachment_image_srcset( attachment_url_to_postid( $hero_image ) ) )
        );
    }
}, 5 );
?>

The order matters: place preload directives early in wp_head, before stylesheets. This gives the browser maximum time to initiate the download.

For modern WordPress setups using Next.js as a front-end or headless blocks, you'll handle preload in your Next.js layout; same principle applies. Use the next/image component's priority prop for above-the-fold images:

php
<Image
  src={heroImage}
  alt="Hero"
  priority={true}
  sizes="100vw"
  style={{ width: '100%', height: 'auto' }}
/>

Measure the effect in Chrome DevTools Lighthouse or WebPageTest. You should see LCP drop by 500–1200ms on slower connections (3G, slow 4G) within the preload scope.

Reduce Render-Blocking CSS and JavaScript

Render-blocking resources are the silent killers of front-end performance. When the browser encounters a stylesheet link or a synchronous script tag, it stops parsing the DOM and processing until that resource is fetched and executed. On a typical WordPress site with 4–6 plugins, you can easily accumulate 200–400KB of render-blocking CSS and JS.

The strategy: defer non-critical CSS, defer non-critical scripts, and inline only what's needed for the critical rendering path.

Deferring stylesheets:

php
<?php
// Load main theme stylesheet normally
wp_enqueue_style( 'theme-main', get_stylesheet_uri() );

// Defer non-critical theme stylesheets (animations, footer, etc.)
wp_enqueue_style(
    'theme-secondary',
    get_template_directory_uri() . '/css/secondary.css',
    array(),
    wp_get_theme()->get( 'Version' )
);

// Add inline script to swap stylesheet media query
add_action( 'wp_footer', function() {
    ?>
    <script>
    const link = document.querySelector('link[href*="secondary.css"]');
    link.media = 'print';
    link.addEventListener('load', function() {
        this.media = 'all';
    });
    </script>
    <?php
}, 99 );
?>

This technique uses the "print" media query trick: the stylesheet loads asynchronously, and once loaded, switches to "all".

Deferring scripts:

php
<?php
wp_enqueue_script(
    'my-plugin-js',
    plugins_url( 'js/plugin.js', __FILE__ ),
    array(),
    '1.0.0',
    true  // Load in footer
);

// Add defer attribute
add_filter( 'script_loader_tag', function( $tag, $handle ) {
    if ( 'my-plugin-js' === $handle ) {
        return str_replace( '>', ' defer>', $tag );
    }
    return $tag;
}, 10, 2 );
?>

Inlining critical CSS: Extract the CSS needed for above-the-fold content and inline it in a style tag in the head. Tools like Critical or CriticalCSS automate this; for a manual approach:

php
<?php
add_action( 'wp_head', function() {
    ?>
    <style>
    /* Critical path CSS: header, hero, nav */
    body { margin: 0; font-family: system-ui; }
    .header { background: #fff; padding: 1rem; }
    .hero { background: url(/hero.jpg) center/cover; min-height: 60vh; }
    </style>
    <?php
}, 5 );
?>

The goal is to get to "First Contentful Paint" (FCP) in under 1.5 seconds. Measure with Lighthouse in throttled (3G) mode to see realistic improvement.

Set Image Dimensions

Cumulative Layout Shift (CLS) is the metric that drives users crazy: content jumping around as images load. The culprit is almost always missing width and height attributes on images.

When you omit dimensions, the browser can't reserve space, so images push other content down as they download and render. Even with CSS aspect-ratio tricks, explicit width and height in HTML give the browser the best chance to allocate space early.

On a SaaS dashboard we optimized, missing image dimensions on the team photos section caused a CLS score of 0.25 (poor). Adding dimensions brought it to 0.08 (good).

For WordPress images:

php
<?php
// In your template or custom block
$image_id = get_post_thumbnail_id();
$image_meta = wp_get_attachment_metadata( $image_id );

if ( $image_meta ) {
    echo wp_get_attachment_image(
        $image_id,
        'large',
        false,
        array(
            'width'  => $image_meta['width'],
            'height' => $image_meta['height'],
        )
    );
}
?>

WordPress's native wp_get_attachment_image() function includes dimensions by default, but ensure your custom image loops do too.

For responsive images: Use CSS aspect-ratio to maintain proportion across breakpoints:

php
img {
  width: 100%;
  height: auto;
  aspect-ratio: 16 / 9;
}

Also set dimensions on any dynamically inserted content (modals, carousels, ads). An example from a ride-hailing partner's site: lazy-loaded passenger photos were causing layout thrashing until we containerized each image with a fixed aspect ratio div.

Limit Third-Party Scripts

Third-party scripts—analytics, chat widgets, ad networks, marketing automation—are the elephant in the room. A travel booking site we audited had 18 external script includes. Total overhead: 650KB and 4.2 seconds of blocking time on 3G.

Third-party scripts are render-blocking by default, run on the main thread, and often fire multiple XHR requests. INP (Interaction to Next Paint) suffers when these scripts execute during user interaction.

Audit your scripts:

php
<?php
// Use Chrome DevTools Coverage tab or WebPageTest to identify unused code
// Then, in your WordPress admin, disable plugins one by one and measure
// Use WP Control or Code Snippets to conditionally load scripts

// Example: Load Google Analytics only on production
if ( wp_get_environment_type() === 'production' ) {
    wp_enqueue_script(
        'google-analytics',
        'https://www.googletagmanager.com/gtag/js?id=GA_ID',
        array(),
        null,
        array(
            'async'       => true,
            'strategyfoo' => true,
        )
    );
}
?>

Defer non-critical third parties: Load chat widgets, feedback tools, or secondary analytics after the page is interactive:

php
<?php
add_action( 'wp_footer', function() {
    ?>
    <script>
    // Load intercom chat widget after page is idle
    if ( 'requestIdleCallback' in window ) {
        requestIdleCallback( function() {
            const s = document.createElement('script');
            s.src = 'https://widget.intercom.io/widget/YOUR_APP_ID';
            s.async = true;
            document.head.appendChild(s);
        });
    } else {
        // Fallback for older browsers
        setTimeout( function() {
            const s = document.createElement('script');
            s.src = 'https://widget.intercom.io/widget/YOUR_APP_ID';
            s.async = true;
            document.head.appendChild(s);
        }, 2000 );
    }
    </script>
    <?php
}, 99 );
?>

Measure before and after with Chrome UX Report or your own RUM setup (Google Analytics 4 event-based reporting, or tools like Sentry Performance Monitoring). Even deferring a 150KB script bundle can cut INP by 300–500ms.

Common Mistakes

  • Preloading too many images: Preload only your critical hero image. Preloading 5+ images wastes bandwidth and can slow down other resources.
  • Not measuring on actual devices: Lighthouse desktop scores can be misleading. Always test on 3G throttling and on real low-end mobile devices.
  • Forgetting aspect-ratio on container divs: Setting width/height on the img tag isn't enough if the parent container has no fixed dimensions. Use a wrapper div with aspect-ratio CSS.
  • Deferring critical functionality scripts: If a script is required for interactivity above the fold, defer it carefully. Test that forms, buttons, and navigation work immediately.
  • Underestimating plugin bloat: A single poorly written WordPress plugin can add 200KB of unoptimized CSS and JS. Audit and remove unused plugins every quarter.

Key Takeaways

  • Preload your hero image early in wp_head to drop LCP by 500–1200ms, especially on slower networks.
  • Defer non-critical CSS and JavaScript using media query tricks and async/defer attributes; inline only critical path CSS.
  • Always set explicit width and height on images to prevent layout shift; use CSS aspect-ratio for responsive sizing.
  • Audit and defer third-party scripts; load non-critical widgets after the page is interactive using requestIdleCallback.
  • Measure continuously with Lighthouse (3G throttled), WebPageTest, and your own RUM strategy to catch regressions early.
ShareXLinkedInReddit

Found this useful? Support the writing.

Buy me a coffee
Written by
Expona Admin
Site administrator

Comments

Loading comments…

Leave a comment

Comments are reviewed before they appear. Be kind and stay on topic.

Related articles