Image optimization in WordPress: lessons from production
We've all inherited WordPress sites where images consume 60–80% of page weight, dragging load times down and frustrating users. In production environments across e-commerce and SaaS, unoptimized images are often the quickest win for performance improvements. Over the past decade, WordPress has quietly built powerful image optimization capabilities directly into core—many teams don't realize they're available or how to use them effectively.
Why images matter in production
A typical WordPress site serves images to browsers running on everything from fiber connections to 4G networks. On slower connections, a 5 MB hero image can add 3+ seconds to first paint. Across millions of requests in production, that multiplies into real cost: slower checkout flows lead to cart abandonment, slower blog loads hurt SEO ranking signals, and slower admin pages frustrate editorial teams.
Modern WordPress sites also need to support multiple screen sizes and pixel densities. A desktop visitor with a retina display might need a 2400 px wide image, while a mobile user on a 375 px screen wastes bandwidth downloading it. This is where WordPress's built-in optimization features become indispensable.
Responsive srcset is built in
WordPress generates multiple image sizes automatically when you upload a media file. By default, it creates thumbnail, medium, medium_large, and large versions. When you render an image using the WordPress template functions, the platform outputs a srcset attribute that tells browsers which image to download based on viewport width and device pixel ratio.
In theme development, always use wp_get_attachment_image() instead of hardcoding <img> tags:
<?php
// Good: generates srcset automatically
echo wp_get_attachment_image( $attachment_id, 'large', false, array(
'loading' => 'lazy',
'decoding' => 'async',
) );
// Avoid: no srcset, no optimization
echo '<img src="' . wp_get_attachment_url( $attachment_id ) . '" alt="Product" />';
?>When a browser sees srcset, it downloads the smallest image that fits the current viewport. A mobile phone with a 375 px wide viewport downloads the 384 px image; a 1920 px desktop downloads the 1536 px version. This alone can reduce image byte transfers by 40–70% for mobile users in production.
You can customize which sizes WordPress generates by registering them in your theme's functions.php:
<?php
add_theme_support( 'post-thumbnails' );
add_image_size( 'hero-desktop', 1920, 1080, true );
add_image_size( 'hero-mobile', 800, 600, true );
add_image_size( 'card-image', 400, 300, true );
?>After registering new sizes, regenerate existing images using a plugin like Regenerate Thumbnails or programmatically during deployment.
WebP and AVIF support
Modern image formats like WebP and AVIF can reduce file size by 20–40% compared to JPEG or PNG while maintaining visual quality. WordPress 6.1+ automatically generates WebP versions of uploaded images, and WordPress 6.4+ supports AVIF.
When you use wp_get_attachment_image(), WordPress outputs picture elements with source tags that let browsers download the best format they support:
<?php
// WordPress generates something like this internally
// <picture>
// <source srcset="image.avif" type="image/avif" />
// <source srcset="image.webp" type="image/webp" />
// <img src="image.jpg" alt="..." />
// </picture>
echo wp_get_attachment_image( $attachment_id, 'large' );
?>A modern Chrome browser downloads the AVIF version; Safari downloads WebP; older browsers fall back to JPEG. This happens transparently without any theme code changes beyond using the correct WordPress functions.
If you're building custom image rendering—say, in an API response or headless setup—ensure your image storage and delivery pipeline supports these formats. Use Imagick or a CDN with automatic format conversion (Cloudflare, Bunny CDN, or similar) to generate WebP and AVIF on the fly.
In production, check that your uploads directory is writable and your server has enough disk space. WebP and AVIF files are generated at upload time, which can temporarily spike CPU and storage usage during bulk imports.
Lazy loading by default
Browsers supporting the native loading="lazy" attribute won't download images until they're about to enter the viewport. This can defer image downloads by seconds on pages with many images, significantly improving initial page load metrics.
WordPress 5.5+ adds lazy loading to all images in post content automatically. For custom images rendered in your theme, enable it explicitly:
<?php
echo wp_get_attachment_image( $attachment_id, 'large', false, array(
'loading' => 'lazy',
) );
?>Modern WordPress versions also add decoding="async", telling browsers they can decode images off the main thread without blocking page rendering.
Be selective with lazy loading in production. Above-the-fold images—hero images, top product images on e-commerce listing pages—should load eagerly to avoid layout shift and improve Largest Contentful Paint (LCP). Use 'loading' => 'eager' for those:
<?php
// Hero image: load immediately
echo wp_get_attachment_image( $hero_id, 'hero-desktop', false, array(
'loading' => 'eager',
) );
// Product thumbnails below the fold: lazy load
foreach ( $product_images as $img_id ) {
echo wp_get_attachment_image( $img_id, 'product-thumb', false, array(
'loading' => 'lazy',
) );
}
?>Compress on upload
WordPress doesn't compress images by default. When a user uploads a 12 MB DSLR photo, WordPress generates thumbnails but doesn't reduce the original file size. In production environments handling thousands of uploads, this balloons storage costs and transfer times.
Use ShortPixel, Imagify, or Smush to automatically compress images on upload. These plugins integrate with WordPress's upload pipeline:
<?php
// Example: Smush API integration happens in the background
// after wp_insert_attachment is called
// In production, configure these plugins with:
// - Automatic compression on upload
// - Removal of EXIF data
// - WebP generation
// - Recursive compression for existing images (bulk operations)
// You can also compress programmatically:
add_filter( 'wp_handle_upload', function( $upload ) {
// Compress using your preferred service
// This runs during the upload process
return $upload;
} );
?>If you prefer not to rely on third-party compression services, run ImageMagick or libvips locally. This requires server resources but keeps files on your infrastructure:
<?php
// After media upload, compress using local tools
add_filter( 'wp_insert_attachment', function( $post_id ) {
$file = get_attached_file( $post_id );
// Example: compress with ImageMagick (requires exec)
// Dangerous in shared hosting; use only in controlled environments
if ( extension_loaded( 'imagick' ) ) {
$image = new Imagick( $file );
$image->setImageCompression( Imagick::COMPRESSION_JPEG );
$image->setImageCompressionQuality( 80 );
$image->writeImage();
}
return $post_id;
} );
?>In production, compression quality matters. Aim for 75–85% JPEG quality; users rarely notice the difference, but file sizes drop 30–50%. For PNG, use lossy compression tools like TinyPNG or PNG crushing.
Common mistakes
- Using
wp_get_attachment_url()for display images: This returns only the original file with no srcset, lazy loading, or format negotiation. Always usewp_get_attachment_image()in themes. - Uploading images larger than needed: If your design never displays images wider than 1200 px, upload images at 1200 px width, not 4000 px. Smaller source files compress better and load faster.
- Forgetting to regenerate thumbnails after adding new image sizes: New sizes only apply to images uploaded after registration. Use Regenerate Thumbnails to update existing media.
- Disabling lazy loading for performance reasons: Lazy loading improves performance. If you're seeing layout shift, fix it with aspect-ratio CSS, not by disabling lazy loading.
- Trusting compression plugins on shared hosting: If your host has CPU limits, bulk compression can trigger throttling. Test on staging first and consider staging bulk operations during off-peak hours.
- Not testing on real mobile networks: Chrome DevTools throttling is helpful but doesn't replicate real 4G variance. Test with actual 4G connections before shipping to production.
Key takeaways
- Use
wp_get_attachment_image()in all themes to automatically benefit from srcset, WebP/AVIF formats, and lazy loading. - Register custom image sizes in
functions.phpand regenerate for existing media when adding new sizes. - Mark above-the-fold images as
loading="eager"to avoid LCP issues; lazy load everything else. - Compress images at upload time using either third-party services or local tools; aim for 75–85% JPEG quality.
- Modern format support (WebP, AVIF) is automatic in WordPress 6.1+; ensure your server generates and serves them without errors.
- Monitor storage costs and CDN bandwidth in production; smaller images directly reduce infrastructure expenses.
- Test image performance on real mobile networks, not just DevTools throttling, before declaring optimization complete.






Comments
Loading comments…