The first time a WooCommerce client of ours opened PageSpeed Insights and saw a 4.0 s LCP next to a 200 KB hero PNG, the diagnosis took thirty seconds and the fix took an afternoon. AVIF conversion plus a fetchpriority="high" hint on the hero brought the page to 1.4 s LCP. The CSS side was harder: an 800 KB stylesheet from a page-builder theme was render-blocking on mobile, and only critical CSS extraction with a 12 KB inline payload pulled FCP under one second. Those two patterns repeat on almost every WordPress site we audit.
This guide walks the full pipeline that actually moves Core Web Vitals on a WordPress install: format choice between AVIF, WebP, and JPEG XL given current browser reality (Safari 16+ ships AVIF, Chrome decodes JPEG XL only behind a flag, Firefox shelved JPEG XL in 2024); plugin selection between ShortPixel, Imagify, Smush, and EWWW with the API rate-limit and DPA quirks each one carries; the 14 KB above-the-fold rule that comes from TCP slow start, not from a marketing slide; and the LiteSpeed Cache settings that produce a measurable LCP delta rather than a placebo.
Where the LCP and FCP losses actually come from
Open the Network panel on a stock WordPress site with a popular page-builder theme and you will see two things eat the LCP budget: a 1-2 MB hero image served as JPEG or PNG, and a 300-800 KB main stylesheet flagged as render-blocking. The third culprit, web fonts loaded without font-display: swap, is smaller in bytes but blocks paint until they arrive.
The hero image is the obvious target because LCP, by definition, is the largest visible element above the fold, and on most WordPress homepages that element is a JPEG. WebP cuts that file by roughly 25-35% at equivalent visual quality, and AVIF cuts it again, often landing 40-55% smaller than the original JPEG depending on content. AVIF support is now broad: Chrome since 85, Firefox since 93, Safari since 16. JPEG XL is the loud absentee: Chromium pulled the decode flag, Firefox parked it indefinitely, only Safari ships it natively, so for a WordPress audience it is not a production format in 2026.
The CSS side is where the 14 KB rule matters. TCP slow start opens a connection at roughly ten packets, around 14 KB after headers, before the first ACK round-trip. Inline anything more than that in your <head> and you stretch the first paint by a full RTT. That is the entire reason critical CSS extractors target a 14 KB ceiling and not a round number from a checklist.
Choosing the image optimisation plugin
Four plugins do most of the work in WordPress image optimisation in 2026, and the choice between them is rarely about compression quality alone. API rate limits, where the conversion runs, and what data leaves your server matter as much as the resulting file size.
ShortPixel Image Optimizer
ShortPixel compresses through its cloud API and ships WebP plus AVIF as standard. Three modes: lossy, glossy, lossless. On photography-heavy sites the glossy preset usually lands within a couple of percent of lossless visual quality at half the bytes. The catch on bulk runs is the API rate ceiling: pushing 10 000+ images through in a day on a small plan triggers throttling, and the plugin’s bulk processor backs off rather than failing visibly, which makes a stalled bulk look like a broken bulk.
Key strengths:
- AVIF generation in production since 2023
- Bulk processing with background queue
- Glossy preset is the practical default for editorial and ecommerce
- Pricing is quota-based and individual
Imagify
Built by the team behind WP Rocket, Imagify integrates with that caching plugin. It offers three compression modes (normal, aggressive, ultra) and generates WebP files automatically.
Key strengths:
- Deep WP Rocket integration for combined optimisation
- Visual comparison tool to preview compression results
- Automatic resizing of oversized uploads
- WebP generation with one click
- Pricing is quota-based (individual, varies by usage)
Smush
Smush by WPMU DEV is the most beginner-friendly option. The free version handles basic compression and lazy loading. The Pro version adds WebP conversion, CDN delivery, and more aggressive optimisation.
Key strengths:
- Free tier with unlimited images (up to 5 MB each)
- Built-in lazy loading
- CDN included with Pro
- Directory-level Smush for non-media-library images
- Pro pricing varies by plan
EWWW Image Optimizer
EWWW is the one plugin in this list that can run entirely on the server without an external API. That matters if your DPA forbids sending uploaded images to a third country, or if you sit behind a firewall that does not allow outbound HTTPS to a SaaS endpoint. Local mode uses jpegoptim, optipng, pngquant and cwebp shelled out from PHP, so you need the binaries on disk; on shared hosts that is a quick support-ticket conversation.
Key strengths:
- Local compression mode with no cloud round-trip
- WebP and AVIF conversion
- ExactDN CDN option for content-negotiation delivery
- WP-CLI commands for cron-driven bulk runs
- Pricing varies by mode, local is free, API plans are individual
Plugin comparison at a glance
| Feature | ShortPixel | Imagify | Smush | EWWW |
|---|---|---|---|---|
| WebP support | Yes | Yes | Pro only | Yes |
| AVIF support | Yes | No | No | Yes |
| Local compression | No | No | No | Yes |
| Bulk processing | Yes | Yes | Yes | Yes |
| WP Rocket integration | Basic | Native | No | Basic |
| Free tier | 100 images/mo | 20 MB/mo | Unlimited (limited) | Local mode |
For most professional WordPress sites, ShortPixel or EWWW offer the best combination of format support, compression quality, and flexibility.
Configuring WebP and AVIF delivery
Installing an optimisation plugin is only half the job. You also need to ensure browsers actually receive the modern format instead of the original JPEG or PNG.
Method 1: Picture element rewriting
The most reliable approach uses the HTML <picture> element to offer multiple formats with automatic fallback:
<picture>
<source srcset="image.avif" type="image/avif">
<source srcset="image.webp" type="image/webp">
<img src="image.jpg" alt="Description" width="800" height="600" loading="lazy">
</picture>
Most optimisation plugins handle this rewriting automatically. ShortPixel and EWWW both inject <picture> tags when configured to do so.
Method 2: Content negotiation via .htaccess
If your server runs Apache or LiteSpeed, you can serve WebP files transparently using content negotiation. The browser sends an Accept header listing supported formats, and the server responds with the best available file:
<IfModule mod_rewrite.c>
RewriteEngine On
# Serve AVIF if available and supported
RewriteCond %{HTTP_ACCEPT} image/avif
RewriteCond %{REQUEST_FILENAME} (.+)\.(jpe?g|png|gif)$
RewriteCond %{REQUEST_FILENAME}.avif -f
RewriteRule (.+)\.(jpe?g|png|gif)$ $1.$2.avif [T=image/avif,E=REQUEST_image,L]
# Serve WebP if available and supported
RewriteCond %{HTTP_ACCEPT} image/webp
RewriteCond %{REQUEST_FILENAME} (.+)\.(jpe?g|png|gif)$
RewriteCond %{REQUEST_FILENAME}.webp -f
RewriteRule (.+)\.(jpe?g|png|gif)$ $1.$2.webp [T=image/webp,E=REQUEST_image,L]
</IfModule>
<IfModule mod_headers.c>
Header append Vary Accept env=REQUEST_image
</IfModule>
This approach is invisible to your HTML and works with any theme or page builder.
Method 3: WordPress filter for programmatic control
For developers who want precise control, you can filter image output at the PHP level:
<?php
declare(strict_types=1);
add_filter('wp_get_attachment_image_attributes', function (array $attr, WP_Post $attachment): array {
$webp_url = preg_replace('/\.(jpe?g|png)$/i', '.webp', $attr['src']);
$upload_dir = wp_get_upload_dir();
$webp_path = str_replace($upload_dir['baseurl'], $upload_dir['basedir'], $webp_url);
if (file_exists($webp_path)) {
$attr['src'] = $webp_url;
if (isset($attr['srcset'])) {
$attr['srcset'] = preg_replace('/\.(jpe?g|png)/i', '.webp', $attr['srcset']);
}
}
return $attr;
}, 10, 2);
Cleaning up WordPress image sizes
WordPress and WooCommerce register numerous image sizes by default. Every uploaded image generates a copy at each registered size, consuming storage and slowing bulk optimisation. Removing sizes you do not use is a quick win.
Remove unused default sizes
<?php
declare(strict_types=1);
add_action('after_setup_theme', function (): void {
// Remove sizes you do not use
remove_image_size('1536x1536');
remove_image_size('2048x2048');
});
add_filter('intermediate_image_sizes_advanced', function (array $sizes): array {
// Keep only the sizes your theme actually uses
$remove = ['medium_large'];
foreach ($remove as $size) {
unset($sizes[$size]);
}
return $sizes;
});
Register only what you need
<?php
declare(strict_types=1);
add_action('after_setup_theme', function (): void {
add_image_size('hero-banner', 1200, 630, true);
add_image_size('card-thumb', 400, 300, true);
add_image_size('logo-small', 200, 0, false);
});
Set maximum upload dimensions
Prevent users from uploading 6000px originals that waste storage and processing time:
<?php
declare(strict_types=1);
add_filter('wp_handle_upload', function (array $upload): array {
$max_width = 2400;
$max_height = 2400;
if (!str_starts_with($upload['type'], 'image/')) {
return $upload;
}
$image = wp_get_image_editor($upload['file']);
if (is_wp_error($image)) {
return $upload;
}
$size = $image->get_size();
if ($size['width'] > $max_width || $size['height'] > $max_height) {
$image->resize($max_width, $max_height, false);
$image->save($upload['file']);
}
return $upload;
});
Understanding critical CSS and render-blocking resources
When a browser loads a WordPress page, it must download and parse the full CSS stylesheet before rendering anything on screen. On a typical WordPress site with a page builder theme, that stylesheet can be 300-500 KB of CSS, most of which applies to elements below the fold or on entirely different pages.
Critical CSS solves this by extracting only the styles required for above-the-fold content and inlining them directly in the HTML <head>. The full stylesheet is then loaded asynchronously, removing it from the critical rendering path.
How critical CSS extraction works
- A tool renders the page in a headless browser at a standard viewport size (typically 1300x900 for desktop, 375x812 for mobile).
- It identifies which CSS rules apply to elements visible in that viewport.
- Those rules are extracted and minified.
- The extracted CSS is inlined in
<style>tags in the document head. - The original stylesheet is loaded with
media="print"and swapped tomedia="all"on load.
Manual critical CSS preload pattern
If you are not using a plugin that handles this automatically, you can implement the pattern with a WordPress filter:
<?php
declare(strict_types=1);
add_action('wp_head', function (): void {
$critical_css_file = get_template_directory() . '/critical.css';
if (!file_exists($critical_css_file)) {
return;
}
$critical_css = file_get_contents($critical_css_file);
if ($critical_css === false) {
return;
}
echo '<style id="critical-css">' . $critical_css . '</style>' . "\n";
}, 1);
add_filter('style_loader_tag', function (string $html, string $handle): string {
// Defer non-critical stylesheets
$defer_handles = ['theme-style', 'woocommerce-layout', 'woocommerce-general'];
if (in_array($handle, $defer_handles, true)) {
$html = str_replace(
"media='all'",
"media='print' onload=\"this.media='all'\"",
$html
);
}
return $html;
}, 10, 2);
Preloading critical assets with link tags
For resources the browser needs immediately, preload hints tell it to start downloading before it discovers the resource naturally in the HTML:
<!-- Preload the hero image -->
<link rel="preload" as="image" href="/wp-content/uploads/hero.webp" type="image/webp">
<!-- Preload critical font -->
<link rel="preload" as="font" href="/wp-content/themes/theme/fonts/main.woff2"
type="font/woff2" crossorigin>
In WordPress, add these programmatically:
<?php
declare(strict_types=1);
add_action('wp_head', function (): void {
if (!is_front_page()) {
return;
}
$hero_image = get_template_directory_uri() . '/images/hero.webp';
echo '<link rel="preload" as="image" href="' . esc_url($hero_image) . '" type="image/webp">' . "\n";
}, 1);
Configuring LiteSpeed Cache for WordPress
LiteSpeed Cache is the fastest WordPress caching plugin available because it operates at the web server level rather than through PHP. If your host runs OpenLiteSpeed or LiteSpeed Enterprise, this plugin communicates directly with the server’s built-in cache engine, bypassing PHP entirely for cached requests.
Essential LiteSpeed Cache settings
Page Cache (Cache tab):
- Enable Cache: On
- Cache Logged-in Users: Off (unless you have membership content)
- Cache Mobile: On (with separate cache for mobile if your theme serves different markup)
- TTL: 604800 (7 days for static content sites), 86400 (1 day for frequently updated sites)
Browser Cache (Cache tab):
- Browser Cache: On
- Browser Cache TTL: 31557600 (1 year, rely on versioned filenames for cache busting)
Object Cache (Cache tab):
- Object Cache: On (requires Redis or Memcached on your server)
- Object Cache TTL: 3600
CSS/JS optimisation settings
Navigate to LiteSpeed Cache, then Optimisation:
- CSS Minify: On
- CSS Combine: On (test carefully, may break some themes)
- Generate Critical CSS: On
- CSS Async Load: On
- JS Minify: On
- JS Combine: Off (frequently causes issues, test before enabling)
- JS Defer: On
- Inline JS After DOM Ready: On
Image optimisation in LiteSpeed Cache
LiteSpeed Cache includes its own image optimisation service through QUIC.cloud:
- Image WebP Replacement: On
- Image Lazy Load: On
- Responsive Placeholder: On
- Viewport Images (first N images excluded from lazy load): 2-3
LiteSpeed .htaccess rules for browser caching
Add these rules to your .htaccess for server-level cache headers:
# LiteSpeed Browser Cache and Compression
<IfModule mod_expires.c>
ExpiresActive On
ExpiresByType image/webp "access plus 1 year"
ExpiresByType image/avif "access plus 1 year"
ExpiresByType image/jpeg "access plus 1 year"
ExpiresByType image/png "access plus 1 year"
ExpiresByType image/svg+xml "access plus 1 year"
ExpiresByType text/css "access plus 1 year"
ExpiresByType application/javascript "access plus 1 year"
ExpiresByType font/woff2 "access plus 1 year"
ExpiresByType font/woff "access plus 1 year"
</IfModule>
<IfModule mod_deflate.c>
AddOutputFilterByType DEFLATE text/html
AddOutputFilterByType DEFLATE text/css
AddOutputFilterByType DEFLATE application/javascript
AddOutputFilterByType DEFLATE application/json
AddOutputFilterByType DEFLATE image/svg+xml
AddOutputFilterByType DEFLATE font/woff
</IfModule>
# Security headers that also improve performance
<IfModule mod_headers.c>
Header set X-Content-Type-Options "nosniff"
Header set Referrer-Policy "strict-origin-when-cross-origin"
Header set Permissions-Policy "interest-cohort=()"
</IfModule>
LiteSpeed crawler configuration
The crawler keeps your cache warm by visiting pages before real users do:
- Crawler: On
- Run Duration: 200 (seconds)
- Interval Between Runs: 600 (seconds)
- Crawl Delay: 500 (milliseconds)
- Server Load Limit: 1
Set these conservatively on shared hosting. On a dedicated server or VPS, you can increase the duration and reduce the interval.
Measuring results with PageSpeed Insights
Optimisation without measurement is guesswork. Google PageSpeed Insights provides both lab data (simulated tests) and field data (real user metrics from the Chrome User Experience Report).
What to check after optimisation
Run PageSpeed Insights on your key pages and verify these items:
Passed audits (green):
- “Serve images in next-gen formats” should pass if WebP/AVIF is configured
- “Efficiently encode images” should pass after compression
- “Properly size images” should pass if you removed unused sizes
- “Eliminate render-blocking resources” should pass with critical CSS
Core Web Vitals:
- LCP target: under 2.5 seconds
- CLS target: under 0.1
- INP target: under 200 milliseconds
Using Chrome DevTools to verify format delivery
Open DevTools (F12), go to the Network tab, and filter by “Img”. Check the “Type” column: you should see webp or avif instead of jpeg or png. If you still see original formats, your rewrite rules or plugin configuration needs adjustment.
Lighthouse performance budget
Set up a performance budget to catch regressions:
[
{
"resourceSizes": [
{ "resourceType": "image", "budget": 300 },
{ "resourceType": "stylesheet", "budget": 50 },
{ "resourceType": "script", "budget": 200 },
{ "resourceType": "total", "budget": 800 }
]
}
]
Values are in kilobytes. This budget ensures your total page weight stays under 800 KB, with images capped at 300 KB.
Advanced techniques for 2026
Fetchpriority for hero images
The fetchpriority attribute tells the browser which images to prioritise during loading. Apply it to your LCP image:
<img src="hero.webp" alt="Hero banner" width="1200" height="630"
fetchpriority="high" decoding="async">
In WordPress, you can add this attribute with a filter:
<?php
declare(strict_types=1);
add_filter('wp_get_attachment_image_attributes', function (array $attr, WP_Post $attachment, string $size): array {
if ($size === 'hero-banner' && is_front_page()) {
$attr['fetchpriority'] = 'high';
$attr['decoding'] = 'async';
unset($attr['loading']); // Remove lazy loading from hero
}
return $attr;
}, 10, 3);
Content-visibility for below-fold sections
The CSS content-visibility property allows the browser to skip rendering off-screen sections until they are needed:
.below-fold-section {
content-visibility: auto;
contain-intrinsic-size: auto 500px;
}
This reduces initial rendering work significantly on long pages with many images.
Responsive images with art direction
For images that need different crops at different breakpoints, use the <picture> element with media queries:
<picture>
<source media="(max-width: 640px)" srcset="hero-mobile.avif" type="image/avif">
<source media="(max-width: 640px)" srcset="hero-mobile.webp" type="image/webp">
<source srcset="hero-desktop.avif" type="image/avif">
<source srcset="hero-desktop.webp" type="image/webp">
<img src="hero-desktop.jpg" alt="Hero image" width="1200" height="630">
</picture>
Performance checklist
Use this checklist to verify your implementation is complete:
- Image optimisation plugin installed and configured
- WebP generation enabled for all new uploads
- AVIF generation enabled (if plugin supports it)
- Bulk optimisation run on existing media library
- Unused image sizes removed
- Maximum upload dimensions set
- .htaccess WebP/AVIF rewrite rules in place
- Critical CSS extraction enabled
- Non-critical stylesheets deferred
- Hero image preloaded with
<link rel="preload"> - Hero image marked with
fetchpriority="high" - LiteSpeed Cache page cache enabled
- LiteSpeed Cache browser cache enabled
- LiteSpeed Cache CSS/JS optimisation configured
- Crawler configured and running
- PageSpeed Insights score verified at 90+
- WebP/AVIF delivery confirmed in DevTools
Two cases that map the pipeline to numbers
The first case was a WooCommerce store on a shared host serving a 200 KB hero PNG, a 1.2 MB carousel of unoptimised JPEGs, and a page-builder stylesheet of around 600 KB on every page. Field data showed mobile LCP at 4.0 s, FCP at 2.8 s. We converted the hero to AVIF (28 KB after glossy compression), added fetchpriority="high" and a matching <link rel="preload">, regenerated thumbnails at the actual sizes the theme used, and ran ShortPixel bulk on the back catalogue. LCP came down to 1.4 s without touching CSS.
The second case was the opposite: images were already AVIF, but FCP refused to drop under 2.4 s on mobile. The single render-blocking offender was an 800 KB main.css containing every page-builder module the site never used. We extracted critical CSS for the homepage and three landing templates with Penthouse, inlined 12 KB in the head, deferred the rest with the media="print" swap pattern, and added font-display: swap to the WOFF2 declarations. FCP came down by roughly 600 ms on a throttled mid-tier Android profile, and the WP Rocket panel stopped flagging “eliminate render-blocking resources”.
The pattern is worth naming explicitly: when LCP is the failing metric, the fix is almost always image-side; when FCP is failing while LCP is acceptable, the fix is CSS-side. Tools that lump the two together produce vague improvements; treating them as separate bottlenecks produces measurable ones.
What WP Rocket, FlyingPress and Perfmatters actually do
These three plugins are not interchangeable, and the marketing pages tend to obscure that. WP Rocket bundles page caching, CSS/JS concatenation and defer, lazy loading, and an integration hook for Imagify; its critical CSS extraction (RUCSS) runs server-side at SaaSWP and falls back gracefully if extraction fails. FlyingPress focuses on serving fonts locally, inlining critical CSS per template, and removing unused CSS at the URL level rather than the site level, which is the better model for sites with diverse layouts. Perfmatters does not cache: it disables WordPress features the site does not need (heartbeat polling, emoji scripts, embed.js, REST endpoints) and adds preconnect, prefetch and DNS-prefetch hints. The three stack cleanly: Perfmatters trims, FlyingPress or WP Rocket handle CSS and cache, an image plugin handles AVIF.
If the host is OpenLiteSpeed or LiteSpeed Enterprise, the calculation changes. LiteSpeed Cache communicates with the server’s built-in cache module and skips PHP entirely on cache hits, which is faster than any plugin running inside PHP can be. The trade-off is that QUIC.cloud handles critical CSS and image conversion off-site, which has its own DPA implications.
Performance pipeline outcomes
Across recent client work the pattern lands in this range. Treat the numbers as a rough envelope rather than a guarantee, since starting condition matters more than the intervention.
| Metric | Before | After |
|---|---|---|
| PageSpeed mobile score | 35-55 | 90-98 |
| LCP | 4.5-8 s | 1.2-2.0 s |
| Total page weight | 3-6 MB | 400-800 KB |
| Number of requests | 80-120 | 25-40 |
| TTFB | 800 ms+ | 150-300 ms |
The combination of modern image formats, critical CSS, and proper cache configuration addresses three independent bottlenecks: file size, render blocking, and server response time. None of them substitutes for the others.
Where wppoland.com fits in
We run this pipeline on production WordPress and WooCommerce sites: server-level LiteSpeed configuration, critical CSS extraction with manual coverage-tab cleanup where Penthouse misses a selector, AVIF delivery with content negotiation, and an audit trail in PageSpeed Insights field data so the changes hold up under real CrUX traffic. Pricing is individual and depends on the size of the media library, the page-builder footprint, and how much of the work is migration versus optimisation in place. If PageSpeed scores are blocking the sites’ rankings, reach out for a performance audit.

