DEV Community

Cover image for WordPress Performance Part 2: Crushing LCP & Mastering Image Optimization for Core Web Vitals
Abiodun Paul Ogunnaike
Abiodun Paul Ogunnaike

Posted on

WordPress Performance Part 2: Crushing LCP & Mastering Image Optimization for Core Web Vitals

In Part 1, we walked through trimming megabytes of script bloat from your-website: stripping Gutenberg editor dependencies from public blocks, banishing @import font waterfalls, and deferring jQuery. Those architectural fixes slashed Total Blocking Time (TBT) and eliminated render-blocking delays.

However, resolving script bloat is only half the battle. When auditing mobile Core Web Vitals, the most visible metric remains Largest Contentful Paint (LCP).

In our Lighthouse audit, mobile LCP clocked in at an agonizing 6.8 seconds, accompanied by a stark recommendation: "Improve image delivery: estimated savings of 1,718 KiB."

In this article, we'll dive into the exact steps we implemented to accelerate hero image delivery, eliminate bandwidth competition, and bring LCP firmly into the green (<2.5s).


Why Images Destroy Mobile Core Web Vitals

Images frequently account for 60% to 80% of a webpage’s total weight. On mobile devices with CPU and network throttling, poor image delivery triggers severe performance bottlenecks:

  1. Delayed LCP Discovery: When a hero banner image is loaded via JavaScript or dynamic sliders, the browser doesn't discover the image URL until after CSS and scripts have executed.
  2. Bandwidth Cannibalization: Slider blocks that eagerly download all slides simultaneously force the browser to divide network bandwidth among hidden slides instead of prioritizing the visible hero image.
  3. Missing Lazy Loading: Below-the-fold cards (event feeds, blog teasers, partner logos) loading eagerly at page initialization clog the network queue.
  4. Oversized Assets: Delivering a 2500×1667 desktop image to a 400px mobile viewport wastes hundreds of kilobytes per image.

Here is the engineering strategy we used to fix these bottlenecks.


Step 1: Prioritize the Hero Banner with fetchpriority="high" & loading="eager"

On modern websites, the hero banner is almost always the Largest Contentful Paint (LCP) element. By default, browsers treat all images with equal priority, queueing them behind other resources.

The Problem: Carousel / Slider Cannibalization

In many custom slider components or carousel blocks, images are generated in a loop without differentiating the active first slide from offscreen slides:

// 🛑 Bad: All slides loaded with default priority
const slidesHtml = slides.map(s => `
  <div class="home-banner-slide">
    <img src="${s.img}" alt="${s.title}" class="home-banner-image" />
  </div>
`).join('');
Enter fullscreen mode Exit fullscreen mode

This causes the browser to download Slide 2, Slide 3, and Slide 4 concurrently with Slide 1, delaying the rendering of the only slide the visitor actually sees.

The Solution: Smart Slide Attribute Splitting

Configure your slider component to treat Slide 1 as a critical asset, while deferring all subsequent slides:

// ✅ Good: First slide prioritized, remaining slides deferred
const slidesHtml = slides.map((s, i) => `
  <div class="home-banner-slide">
    <img 
      src="${s.img}" 
      alt="${s.title}" 
      class="home-banner-image" 
      ${i === 0 
        ? 'fetchpriority="high" loading="eager"' 
        : 'loading="lazy" decoding="async"'} 
    />
  </div>
`).join('');
Enter fullscreen mode Exit fullscreen mode
  • fetchpriority="high": Informs the browser's preload scanner that this image has maximum network priority.
  • loading="eager": Instructs the browser not to delay fetching the image.
  • loading="lazy" & decoding="async": Ensures offscreen slides don't consume a single byte of bandwidth until the user starts navigating the carousel.

Step 2: Preload the LCP Hero Image in <head>

Even with fetchpriority="high", if your hero banner is rendered inside a Web Component or dynamic block, the browser must parse the HTML body before discovering the image URL.

To achieve lightning-fast LCP, the browser should start downloading the hero banner the instant the initial HTML document response arrives.

The Solution: Server-Side Preload Hint

In your theme's functions.php, extract the hero image URL from the page's block attributes and emit a <link rel="preload"> tag directly in wp_head:

add_action('wp_head', function () {
    // Only target the front page or home page
    if (!is_admin() && (is_front_page() || is_home())) {
        global $post;
        if ($post instanceof WP_Post && has_block('your-website/home-banner-block', $post)) {
            $blocks = parse_blocks($post->post_content);
            foreach ($blocks as $block) {
                if ($block['blockName'] === 'your-website/home-banner-block' && !empty($block['attrs']['slides'][0]['imageSrc'])) {
                    $hero_img = esc_url($block['attrs']['slides'][0]['imageSrc']);
                    echo '<link rel="preload" as="image" href="' . $hero_img . '" fetchpriority="high">' . "\n";
                    break;
                }
            }
        }
    }
}, 1);
Enter fullscreen mode Exit fullscreen mode

Why this matters: This moves hero image discovery from step 15 in the browser waterfall directly to step 1. The image begins downloading before external stylesheets or JavaScript bundles are even evaluated.


Step 3: Enforce Native Lazy Loading Across All Below-the-Fold Blocks

A typical homepage contains multiple dynamic sections below the fold:

  • Upcoming Events
  • Community News
  • Impact Stories
  • Partner Logos & Grids

If these cards render standard <img> tags without explicit loading attributes, the browser queues up dozens of image requests during initial page load, choking the main thread.

The Solution: Audit Dynamic Block Templates

Audit your theme's PHP block render functions and add native loading="lazy" and decoding="async":

<!-- In your custom card or event block template -->
<div class="card__img-wrapper">
    <img 
        class="card__img" 
        src="<?php echo esc_url($image_url); ?>" 
        alt="<?php echo esc_attr($title); ?>" 
        loading="lazy" 
        decoding="async"
    >
</div>
Enter fullscreen mode Exit fullscreen mode

Global WordPress Attachment Filter

To ensure that all standard WordPress media library images rendered via core functions also benefit from non-blocking decoding, add this hook in functions.php:

// Ensure non-blocking image decoding across all attachment images
add_filter('wp_get_attachment_image_attributes', function ($attr) {
    if (!isset($attr['decoding'])) {
        $attr['decoding'] = 'async';
    }
    return $attr;
}, 10, 1);
Enter fullscreen mode Exit fullscreen mode
  • decoding="async": Allows the browser to decode image raster data off the main thread, preventing UI jank and reducing Total Blocking Time during scrolling.

Step 4: Responsive Image Sizing & Layout Stability (CLS)

Lighthouse often flags: "Image file is larger than it needs to be for its displayed dimensions."

Serving a 2000px wide image into a 300px mobile card wastes data and forces mobile CPUs to perform expensive client-side image downscaling.

1. Leverage WordPress Responsive srcset

Instead of using raw image URLs with 'full', always register and request appropriate WordPress image dimensions:

// In functions.php
add_image_size('card-thumbnail', 600, 400, true);
add_image_size('hero-banner-mobile', 768, 500, true);

// In your template: use wp_get_attachment_image for automatic srcset & sizes
echo wp_get_attachment_image($thumbnail_id, 'card-thumbnail', false, [
    'class'   => 'card__img',
    'loading' => 'lazy',
]);
Enter fullscreen mode Exit fullscreen mode

2. Prevent Cumulative Layout Shift (CLS)

When images lack explicit dimensions or aspect ratios, the browser cannot reserve space for them before they load, causing jarring layout shifts as images pop into view.

Add modern CSS aspect ratio rules to your image containers:

.card__img-wrapper {
    position: relative;
    width: 100%;
    aspect-ratio: 16 / 9;
    overflow: hidden;
    background-color: #f0f0f0; /* Low-contrast placeholder */
}

.card__img {
    width: 100%;
    height: 100%;
    object-fit: cover;
}
Enter fullscreen mode Exit fullscreen mode

By defining aspect-ratio, the browser reserves exact bounding boxes before the image binary arrives, locking CLS at 0.000.


Step 5: Adopt Next-Gen Image Formats (WebP & AVIF)

Traditional formats like PNG and JPEG are inefficient for web delivery:

  • WebP: Offers 25%–35% smaller file sizes than JPEG with identical visual quality.
  • AVIF: Provides up to 50% compression savings compared to JPEG and supports superior color depth and high dynamic range.

WordPress Native Modern Format Support

Starting in WordPress 5.8 (WebP) and WordPress 6.5 (AVIF), WordPress natively supports next-gen image uploads:

// In functions.php: Enable AVIF and WebP generation on upload (WP 6.5+)
add_filter('image_editor_output_format', function ($formats) {
    $formats['image/jpeg'] = 'image/webp';
    $formats['image/png']  = 'image/webp';
    return $formats;
});
Enter fullscreen mode Exit fullscreen mode

For existing media libraries, automated image optimization or server-level rewrites can convert your legacy library to WebP/AVIF without changing media URLs in your database.


The Combined Impact of Phase 1 + Phase 2

Combining Phase 1 (Script & Cache Optimization) and Phase 2 (Image & LCP Acceleration) delivers a complete performance transformation:

Metric Before Optimization After Phase 1 & Phase 2 Impact
Mobile Score 35 85–95+ 🟢 Massive Jump
Total Blocking Time (TBT) 9,660 ms < 200 ms âš¡ Instant Main Thread
Largest Contentful Paint (LCP) 6.8 s < 1.8 s 🚀 73% Faster Rendering
Total Page Weight ~4.2 MB ~1.2 MB 📉 Over 65% Bandwidth Saved
Cumulative Layout Shift (CLS) 0.001 0.000 🎯 Rock-Solid Layout

Summary Checklist for Your WordPress Project

  • [x] Preload the LCP hero banner in wp_head with fetchpriority="high".
  • [x] Set fetchpriority="high" and loading="eager" on the first carousel slide; set loading="lazy" on all remaining slides.
  • [x] Add loading="lazy" and decoding="async" across all below-the-fold custom blocks.
  • [x] Define aspect-ratio on all image containers to permanently prevent layout shifts.
  • [x] Serve responsive srcset sizes and leverage modern WebP/AVIF formats.

WebVitals #WebDev #FrontendDevelopment #PageSpeed

PART 1 - https://dev.to/abbeymaniak/a-practical-walkthrough-of-real-world-wordpress-performance-optimizations-46fm

Top comments (0)