WordPress Image Optimization: Formats, Compression, and Lazy Loading
Images are usually the heaviest thing on your page. Here's how to make them smaller without making them look worse.
Your Images Are Probably the Problem
Here’s a stat that surprises people: images account for roughly 50% of the total weight of an average web page. The HTTP Archive puts the median page weight for images at around 1 MB — out of a total ~2.4 MB. On image-heavy pages like portfolios or ecommerce product listings, that number can easily hit 5-10 MB.
That matters because page weight directly correlates with load time, which directly correlates with bounce rate. Google’s data shows that as page load goes from 1 second to 3 seconds, bounce probability increases by 32%. From 1 to 5 seconds? 90%.
So if your WordPress site is slow and you haven’t tackled images yet, that’s almost certainly where the biggest wins are hiding.
Let’s go through everything: formats, compression, responsive delivery, lazy loading, CDNs, and the layout shift gotcha that most people miss.
Image Formats: Picking the Right One
Not all image formats are created equal. Using the wrong format for a given image can mean 2-5x larger file sizes for no visual benefit. Here’s the breakdown.
JPEG
The workhorse. JPEG uses lossy compression and works great for photographs and complex images with lots of color variation. It doesn’t support transparency.
A typical high-quality JPEG photo at 1200px wide comes in at around 150-300 KB. That’s decent, but we can do better with modern formats.
Use JPEG when: You need maximum compatibility and your audience includes very old browsers. Otherwise, you probably want WebP or AVIF instead.
PNG
PNG uses lossless compression. Files are larger than JPEG, but you get pixel-perfect reproduction and transparency support.
The problem is that people use PNG for everything. That hero photo doesn’t need to be a 2 MB PNG. PNGs are meant for graphics with sharp edges, text overlays, logos, icons, and screenshots — things where JPEG compression artifacts would be visible.
A 1200px photo saved as PNG might be 1-3 MB. The same photo as JPEG is 200 KB. That difference adds up fast across a whole page.
Use PNG when: You need transparency, or the image has sharp lines/text where JPEG artifacts would show. For photos, don’t.
WebP
WebP is Google’s format and it’s genuinely good. It supports both lossy and lossless compression, transparency, and even animation. Lossy WebP is typically 25-35% smaller than equivalent-quality JPEG. Lossless WebP is about 25% smaller than PNG.
Real numbers from a test we ran on a client’s product images:
Original JPEG (quality 80): 187 KB
WebP (quality 80): 128 KB → 32% smaller
AVIF (quality 60): 74 KB → 60% smaller
Browser support for WebP is excellent — it’s at about 97% globally as of early 2026. The only holdouts are very old Safari versions and some niche browsers. For practical purposes, you can treat WebP as universally supported.
Use WebP when: Pretty much always. It’s the safe modern default. If you can only pick one next-gen format, pick WebP.
AVIF
AVIF is the new kid, based on the AV1 video codec. It beats WebP on compression — typically 30-50% smaller than WebP at equivalent visual quality. That’s a massive difference.
The catch is browser support. It’s at about 93% globally — Chrome, Firefox, and Safari (since 16.4) all support it. But encoding is slower than WebP, which matters for on-the-fly conversion. And some WordPress plugins still have spotty AVIF support.
Use AVIF when: You want the absolute best compression and you can serve WebP or JPEG as a fallback. The <picture> element makes this easy.
The Comparison Table
Here’s a practical comparison using a 1200x800 product photo:
Format Quality File Size Savings vs JPEG
─────────────────────────────────────────────────────
JPEG 80 187 KB baseline
PNG lossless 1,420 KB -659% (bigger)
WebP 80 128 KB 32% smaller
AVIF 60 74 KB 60% smaller
That AVIF file is 60% smaller than JPEG with virtually identical visual quality. On a product listing page with 20 images, that’s the difference between 3.7 MB and 1.5 MB just in images.
Compression Tools and Plugins
You’ve got two paths here: compress images before uploading, or let a plugin handle it automatically. Both work. Doing both is even better.
WordPress Plugins
ShortPixel is our go-to recommendation. It compresses on upload, converts to WebP and AVIF, and serves the right format via <picture> elements or URL rewriting. The free tier gives you 100 images/month, and the paid plans are cheap. It also handles bulk optimization of your existing media library.
Imagify (by the WP Rocket team) is another solid option. Similar feature set to ShortPixel — lossy/glossy/lossless compression, WebP conversion, bulk optimization. The integration with WP Rocket is particularly smooth if you’re already using that for caching.
EWWW Image Optimizer deserves a mention because it can do compression locally on your server instead of sending images to a cloud API. That’s useful if you have privacy requirements or a very large library where API credits get expensive.
All three of these will:
- Compress images on upload
- Convert to WebP (and AVIF, in most cases)
- Serve the appropriate format to each browser
- Bulk optimize your existing library
Pick one. Don’t install multiple image optimization plugins — they’ll conflict.
Standalone Tools
Squoosh (squoosh.app) is a browser-based tool from the Chrome team. It’s fantastic for one-off compressions and for experimenting with quality settings. You can drag in an image, try different formats and quality levels, and see the visual difference side-by-side. Great for figuring out the lowest quality level you can get away with for a specific image.
ImageOptim (Mac) and FileOptimizer (Windows) are desktop apps for batch processing. ImageOptim is particularly nice — just drag a folder of images onto it and it strips metadata, optimizes compression, and overwrites the originals.
CLI Tools
If you’re comfortable with the command line, you can automate everything. Here are the tools we use:
cwebp — Google’s official WebP encoder:
# Convert a single JPEG to WebP at quality 80
cwebp -q 80 photo.jpg -o photo.webp
# Batch convert all JPEGs in a directory
for f in *.jpg; do cwebp -q 80 "$f" -o "${f%.jpg}.webp"; done
avifenc — AVIF encoder (part of libavif):
# Convert to AVIF with quality 60, speed 6 (0=slowest/best, 10=fastest)
avifenc --min 20 --max 40 -s 6 photo.jpg photo.avif
# Batch convert
for f in *.jpg; do avifenc --min 20 --max 40 -s 6 "$f" "${f%.jpg}.avif"; done
jpegoptim and optipng — for optimizing without changing formats:
# Lossless JPEG optimization (strips metadata, optimizes Huffman tables)
jpegoptim --strip-all *.jpg
# Lossy JPEG, target 85% quality
jpegoptim --max=85 --strip-all *.jpg
# Lossless PNG optimization
optipng -o5 *.png
You can wire these into a build step or a Git hook if you’re working with a custom theme. We have a Makefile target that runs all uploads through cwebp and jpegoptim before they ever hit the WordPress media library.
Responsive Images: Stop Serving Desktop Images to Phones
This is one of the highest-impact optimizations, and WordPress actually handles a lot of it automatically — if you understand how it works and don’t accidentally break it.
When you upload an image to WordPress, it generates multiple sizes by default: thumbnail (150x150), medium (300x300), medium_large (768px wide), large (1024px wide), and the original. When you insert an image into a post, WordPress automatically adds srcset and sizes attributes.
Here’s what that looks like in the HTML:
<img
src="photo-1024x683.jpg"
srcset="
photo-300x200.jpg 300w,
photo-768x512.jpg 768w,
photo-1024x683.jpg 1024w,
photo-1536x1024.jpg 1536w,
photo-2048x1365.jpg 2048w"
sizes="(max-width: 1024px) 100vw, 1024px"
width="1024"
height="683"
alt="Product photo"
/>
The browser looks at srcset, checks the viewport width and device pixel ratio, and picks the smallest image that’ll look sharp. A phone on a 3G connection gets the 300px version. A desktop on fiber gets the 1024px or larger version. This happens automatically.
Where WordPress Gets It Wrong
The default sizes attribute is often too generic. WordPress typically outputs sizes="(max-width: 1024px) 100vw, 1024px" — which tells the browser “this image is 100% of the viewport width up to 1024px.” That’s only accurate if the image actually spans the full width.
If your image sits inside a content column that maxes out at, say, 720px, the browser is selecting a larger image than necessary. You can fix this in your theme:
// In your theme's functions.php
add_filter('wp_calculate_image_sizes', function($sizes, $size, $image_src, $image_meta, $attachment_id) {
// For images in the content area, cap at the content width
if (is_singular()) {
$sizes = '(max-width: 720px) 100vw, 720px';
}
return $sizes;
}, 10, 5);
Adding Custom Image Sizes
If your theme design calls for specific dimensions (like a 600px-wide card image), register them:
add_action('after_setup_theme', function() {
add_image_size('card-image', 600, 400, true); // hard crop
add_image_size('hero-large', 1920, 800, true);
});
WordPress will generate these sizes on upload, and they’ll be included in the srcset. More options for the browser to choose from means better size matching.
The <picture> Element for Format Switching
If you want to serve AVIF with a WebP fallback and a JPEG last resort, the <picture> element is how:
<picture>
<source
type="image/avif"
srcset="
photo-400.avif 400w,
photo-800.avif 800w,
photo-1200.avif 1200w"
sizes="(max-width: 800px) 100vw, 800px"
/>
<source
type="image/webp"
srcset="
photo-400.webp 400w,
photo-800.webp 800w,
photo-1200.webp 1200w"
sizes="(max-width: 800px) 100vw, 800px"
/>
<img
src="photo-800.jpg"
srcset="
photo-400.jpg 400w,
photo-800.jpg 800w,
photo-1200.jpg 1200w"
sizes="(max-width: 800px) 100vw, 800px"
width="800"
height="533"
alt="Product photo"
loading="lazy"
/>
</picture>
The browser picks the first <source> it supports, then selects the right size from that source’s srcset. Older browsers ignore the <source> elements and fall back to the <img>.
Most image optimization plugins (ShortPixel, Imagify, EWWW) can generate this markup automatically. You don’t have to hand-code it.
Lazy Loading: Load Images Only When Needed
Lazy loading defers the download of offscreen images until the user scrolls near them. A page with 20 images below the fold? The browser only downloads the 2-3 visible ones on initial load. The rest load as you scroll.
Native Lazy Loading
Modern browsers support the loading="lazy" attribute natively. No JavaScript needed:
<img src="photo.webp" loading="lazy" width="800" height="600" alt="Description" />
That’s it. The browser handles the intersection logic, determines when to start fetching, and manages the network requests. WordPress has added loading="lazy" to content images by default since version 5.5.
The LCP Trap: When NOT to Lazy Load
Here’s the mistake we see on almost every site we audit: lazy loading the hero image or the above-the-fold featured image. That image is the LCP element — the thing Google measures as your primary loading metric. Slapping loading="lazy" on it tells the browser “don’t prioritize this,” which is the exact opposite of what you want.
WordPress 5.9+ is smart enough to skip loading="lazy" on the first content image in a post. But it doesn’t always get it right, especially with custom themes or page builders.
You can manually control it. For your LCP image, do this:
<!-- LCP image: eager load + high fetch priority -->
<img
src="hero.webp"
loading="eager"
fetchpriority="high"
width="1200"
height="600"
alt="Hero banner"
/>
And in WordPress, you can filter it:
// Remove lazy loading from specific images
add_filter('wp_img_tag_add_loading_attr', function($value, $image, $context) {
// Don't lazy-load if the image has the 'hero-image' class
if (strpos($image, 'hero-image') !== false) {
return false; // prevents loading="lazy" from being added
}
return $value;
}, 10, 3);
A Simple Rule
- Above the fold (visible without scrolling):
loading="eager"andfetchpriority="high"on the LCP image. Other above-fold images can useloading="eager"or just omit the attribute entirely. - Below the fold:
loading="lazy"on everything.
If you’re not sure, open your site in Chrome DevTools, run a Lighthouse audit, and check which element is flagged as LCP. That one should never be lazy loaded.
Lazy Loading Background Images
The loading attribute only works on <img> and <iframe> elements. CSS background images aren’t affected. If you’ve got heavy background images below the fold, you’ll need JavaScript or a technique like this:
/* Default: no background image */
.section-bg {
background-color: #f5f5f5;
}
/* Load background when section is visible */
.section-bg.is-visible {
background-image: url('background.webp');
}
// Simple intersection observer for background images
const lazyBackgrounds = document.querySelectorAll('.section-bg');
const observer = new IntersectionObserver((entries) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
entry.target.classList.add('is-visible');
observer.unobserve(entry.target);
}
});
}, { rootMargin: '200px' });
lazyBackgrounds.forEach(el => observer.observe(el));
CDN Image Delivery
A CDN puts copies of your images on servers around the world. A visitor in Tokyo gets images from a Tokyo server instead of your origin in Virginia. That alone can cut image load times in half for international visitors.
But modern image CDNs do more than just caching. They can transform images on the fly.
Cloudflare Polish
If you’re already using Cloudflare (and you probably should be — the free tier is generous), Polish is their image optimization feature. It automatically compresses images and converts to WebP based on the visitor’s browser. It works at the CDN edge, so your origin server doesn’t have to do any extra work.
Polish comes with the Pro plan ($20/month). It’s one of the easiest image optimizations you can enable — flip a switch in the dashboard and every image on your site gets optimized on delivery.
Cloudflare also offers Image Resizing on the Business plan and above, which lets you resize and transform images via URL parameters. Useful if you want responsive images without generating all the sizes upfront.
Bunny CDN Optimizer
Bunny CDN is affordable and fast. Their image optimization add-on (Bunny Optimizer) does WebP/AVIF conversion, resizing, and compression on the edge. At $9.50/month for the CDN plus a small per-image fee for optimization, it’s hard to beat on price.
The nice thing about Bunny is that it can act as a pull zone for your WordPress uploads directory. Set it up, point your image URLs to the Bunny CDN hostname, and it handles the rest. No WordPress plugin required (though their plugin makes setup easier).
imgix
imgix is the power tool. It’s a dedicated image CDN that supports hundreds of URL-based transformations — resize, crop, format conversion, face detection, color adjustment, watermarks, you name it.
https://your-site.imgix.net/photo.jpg?w=800&auto=format,compress&q=75
That URL serves an 800px-wide image, automatically picks the best format for the browser, compresses it, and targets quality 75. All on the fly.
imgix is more expensive than Cloudflare or Bunny, but it’s popular with agencies and larger sites where image flexibility matters. They have a WordPress plugin that rewrites your image URLs to go through imgix.
Which CDN to Pick?
- Already on Cloudflare? Enable Polish. Done.
- Budget-conscious? Bunny CDN Optimizer.
- Need advanced transformations? imgix.
- Just want something simple? Even a basic CDN like Cloudflare’s free tier (without Polish) helps — faster delivery even without optimization.
CLS Prevention: Always Set Width and Height
This one is quick but important. CLS (Cumulative Layout Shift) measures how much stuff moves around on the page while it loads. Images without dimensions are a top cause of layout shift.
Here’s what happens: the browser starts rendering the page, encounters an <img> tag with no width or height, doesn’t know how big the image will be, so it allocates zero space. When the image finally loads, the content below it gets shoved down. That’s a layout shift, and Google penalizes you for it.
The fix is dead simple. Always include width and height attributes:
<!-- Bad: browser doesn't know the image dimensions until it loads -->
<img src="photo.webp" alt="Product" />
<!-- Good: browser reserves the exact space needed -->
<img src="photo.webp" width="800" height="600" alt="Product" />
Modern CSS handles the rest. As long as you have this in your stylesheet (and most themes do):
img {
max-width: 100%;
height: auto;
}
The browser uses the width and height attributes to calculate the aspect ratio, reserves the correct space in the layout, and scales the image responsively within its container. No shift.
WordPress adds width and height automatically to images inserted through the editor. The problems happen with:
- Theme-coded images where the developer forgot the attributes
- CSS background images (use
aspect-ratioin CSS instead) - Dynamically loaded images from JavaScript or AJAX
Check your CLS score in PageSpeed Insights. If it’s above 0.1, images without dimensions are a likely culprit.
For CSS background images or containers that hold images, the aspect-ratio property is your friend:
.image-container {
aspect-ratio: 16 / 9;
width: 100%;
overflow: hidden;
}
This reserves the correct space even before the image loads.
Putting It All Together
Here’s the checklist we use when optimizing images on a WordPress site:
- Install an image optimization plugin — ShortPixel or Imagify. Enable WebP conversion. Enable AVIF if available. Run bulk optimization on the existing library.
- Check your formats — JPEGs for photos, PNGs only where transparency is needed, WebP/AVIF served to supported browsers via the plugin.
- Audit responsive images — Make sure
srcsetis present on content images. Adjust thesizesattribute if your theme’s content area is narrower than what WordPress assumes. - Fix lazy loading — Confirm
loading="lazy"is on below-fold images and NOT on the LCP image. Addfetchpriority="high"to the LCP image. - Set up a CDN — Even a basic one helps. Enable image optimization features if available.
- Check dimensions — Every
<img>tag should havewidthandheight. Test CLS in PageSpeed Insights.
Done right, these optimizations typically cut total image payload by 50-70%. On image-heavy pages, we’ve seen 80%+ reductions. That translates directly to faster LCP, lower bandwidth costs, and better Core Web Vitals scores.
Need Help With This?
Image optimization is one piece of a bigger performance puzzle. If your WordPress site is slow and you want someone to handle the full optimization — server config, caching, database, images, Core Web Vitals — check out our speed optimization service.
Or if you just want to talk through what’s slowing your site down, get in touch. We’ll take a look and tell you where the biggest opportunities are.
Need help with your WordPress site?
We can help with the stuff covered in this post. Message us and we'll figure it out.