Optimize Core Web Vitals — LCP, CLS, and INP — in WordPress

Google’s Core Web Vitals are three user-experience metrics that directly influence Search ranking signals as part of the Page Experience update: Largest Contentful Paint (LCP) measures how quickly the largest above-the-fold element renders (target: under 2.5 seconds), Cumulative Layout Shift (CLS) measures visual stability by summing unexpected layout shifts (target: under 0.1), and Interaction to Next Paint (INP) replaced First Input Delay in 2024 as the responsiveness metric, measuring the delay between a user interaction and the next paint (target: under 200ms). For WordPress sites, LCP is most often the hero image or the site logo — the highest-impact optimization is adding fetchpriority="high" and loading="eager" to the LCP image element, adding a <link rel="preload"> tag in <head>, and ensuring the image is served in WebP format at the correct display dimensions with no oversizing. WordPress 6.3 introduced automatic LCP image detection and fetchpriority injection via the wp_get_attachment_image function — verify the feature is active and not overridden by theme code. CLS in WordPress is most commonly caused by images without explicit width and height attributes (which reserve layout space before the image loads), web fonts swapping from a fallback to the loaded font, and above-the-fold ads or embeds inserted without a reserved container. WordPress’s the_content() filter processes img tags through wp_lazy_loading_enabled() and wp_img_tag_add_width_and_height() since WordPress 5.5 — both must be active. INP bottlenecks in WordPress are typically caused by large JavaScript bundles that block the main thread during parsing — deferring or asynchronously loading scripts with wp_script_add_data( $handle, 'strategy', 'defer' ) (WordPress 6.3 script loading strategies) moves long tasks off the critical rendering path and reduces INP. The structured data post covers the schema layer; Core Web Vitals cover the performance layer of Google’s ranking signals.

Problem: A WordPress blog scores 45/100 on Google PageSpeed Insights mobile — LCP is 5.2s (hero image loaded without priority), CLS is 0.28 (images missing dimensions, web font swap), and INP is 380ms (three large JavaScript bundles loaded synchronously in <head>).

Solution: Preload and prioritize the LCP image, add explicit dimensions to all content images, use font-display: swap with a size-adjusted fallback to minimize CLS from font loading, and apply WordPress 6.3’s defer/async script loading strategy to non-critical JavaScript.

// ── LCP: preload hero image and set fetchpriority ─────────────────────────
add_action( 'wp_head', function(): void {
    if ( ! is_singular() ) return;

    $thumbnail_id = get_post_thumbnail_id();
    if ( ! $thumbnail_id ) return;

    $src = wp_get_attachment_image_url( $thumbnail_id, 'large' );
    $srcset = wp_get_attachment_image_srcset( $thumbnail_id, 'large' );
    if ( ! $src ) return;

    printf(
        '' . PHP_EOL,
        esc_url( $src ),
        esc_attr( $srcset )
    );
}, 1 ); // priority 1 = very early in 

// Set fetchpriority="high" on the post thumbnail (LCP element)
add_filter( 'wp_get_attachment_image_attributes', function( array $attr, WP_Post $attachment, $size ): array {
    // Only boost priority for the post thumbnail on singular views
    if ( is_singular() && get_post_thumbnail_id() === $attachment->ID ) {
        $attr['fetchpriority'] = 'high';
        $attr['loading']       = 'eager';  // override lazy loading for LCP image
    }
    return $attr;
}, 10, 3 );

// ── CLS: ensure all images have width/height attributes ───────────────────
// WordPress adds these automatically via wp_img_tag_add_width_and_height()
// Verify it's not disabled by a plugin:
add_filter( 'wp_img_tag_add_width_and_height', '__return_true' );

// ── INP: use WordPress 6.3 script loading strategies ─────────────────────
add_action( 'wp_enqueue_scripts', function(): void {
    // Register with 'defer' strategy — script loads after HTML parsing
    wp_enqueue_script(
        'myplugin-interactions',
        get_theme_file_uri( 'js/interactions.min.js' ),
        [],
        '1.0',
        [ 'strategy' => 'defer', 'in_footer' => true ]
    );

    // Register with 'async' strategy — for independent analytics scripts
    wp_enqueue_script(
        'myplugin-analytics',
        get_theme_file_uri( 'js/analytics.min.js' ),
        [],
        '1.0',
        [ 'strategy' => 'async', 'in_footer' => false ]
    );
} );

/* ── CLS: font loading with size-adjusted fallback ──────────────────────── */

/* 1. Declare the web font */
@font-face {
    font-family: 'MyFont';
    src: url('/wp-content/fonts/myfont.woff2') format('woff2');
    font-weight: 400;
    font-style: normal;
    font-display: swap;     /* show fallback immediately, swap when loaded */
    unicode-range: U+0000-00FF;
}

/* 2. Size-adjusted fallback: tune metrics to match web font dimensions */
@font-face {
    font-family: 'MyFont-fallback';
    src: local('Arial');
    ascent-override:  90%;   /* adjust to match web font metrics */
    descent-override: 20%;
    line-gap-override: 0%;
    size-adjust: 105%;       /* scale fallback to reduce layout shift on swap */
}

body {
    font-family: 'MyFont', 'MyFont-fallback', Arial, sans-serif;
}

/* ── CLS: reserve space for embeds and ads before they load ─────────────── */
.ad-slot {
    min-height: 250px;
    contain: layout;    /* prevent ad content from shifting surrounding layout */
}

NOTE: Use the Chrome DevTools Performance panel with “Web Vitals” enabled to identify the exact LCP element, the CLS-causing layout shifts (shown as red bars on the experience track), and the long tasks contributing to INP — field data from Google Search Console’s Core Web Vitals report often differs from lab data because it reflects real users on slower devices and networks. Fix lab data first, then monitor field data trends for 28 days (the CrUX data window) to confirm the improvements register in Google’s ranking signals.