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:
- 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.
- 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.
- Missing Lazy Loading: Below-the-fold cards (event feeds, blog teasers, partner logos) loading eagerly at page initialization clog the network queue.
- 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('');
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('');
-
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);
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>
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);
-
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',
]);
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;
}
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;
});
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_headwithfetchpriority="high". - [x] Set
fetchpriority="high"andloading="eager"on the first carousel slide; setloading="lazy"on all remaining slides. - [x] Add
loading="lazy"anddecoding="async"across all below-the-fold custom blocks. - [x] Define
aspect-ratioon all image containers to permanently prevent layout shifts. - [x] Serve responsive
srcsetsizes and leverage modern WebP/AVIF formats.
WebVitals #WebDev #FrontendDevelopment #PageSpeed
Top comments (0)