WordPress Image Performance Complete Guide
Master image optimization in WordPress. Learn core features, plugins, WebP/AVIF support, responsive images, WooCommerce optimization, and performance best practices.
WordPress powers over 40% of the web, and images are often the largest performance bottleneck. This guide covers everything from WordPress core features to advanced optimization techniques.
WordPress Core Image Features
Built-in Image Handling
WordPress automatically:
- Generates multiple image sizes on upload
- Creates responsive srcset for content images
- Adds lazy loading (since WordPress 5.5)
- Supports WebP uploads (since WordPress 5.8)
Default Image Sizes
| Size | Dimensions | Use Case |
|---|---|---|
| Thumbnail | 150×150 | Admin, widgets |
| Medium | 300×300 | Content flow |
| Medium Large | 768×auto | Responsive |
| Large | 1024×1024 | Full-width content |
| Full | Original | Direct use |
Customizing Image Sizes
// functions.php
// Modify default sizes
update_option('thumbnail_size_w', 150);
update_option('thumbnail_size_h', 150);
update_option('thumbnail_crop', 1);
update_option('medium_size_w', 400);
update_option('medium_size_h', 400);
update_option('large_size_w', 1200);
update_option('large_size_h', 1200);
// Add custom sizes
add_image_size('hero', 1920, 800, true);
add_image_size('card', 600, 400, true);
add_image_size('gallery-thumb', 300, 300, true);
// Make sizes available in editor
add_filter('image_size_names_choose', function($sizes) {
return array_merge($sizes, [
'hero' => 'Hero Banner',
'card' => 'Card Image',
'gallery-thumb' => 'Gallery Thumbnail'
]);
});
WebP and AVIF Support
WordPress WebP Support
Since WordPress 5.8, WebP is fully supported:
// Check WebP support
function check_webp_support() {
$mime_types = get_allowed_mime_types();
return isset($mime_types['webp']);
}
// WordPress 5.8+ automatically supports WebP
// No additional configuration needed for basic support
Enabling AVIF Support
// functions.php
add_filter('upload_mimes', function($mimes) {
$mimes['avif'] = 'image/avif';
return $mimes;
});
add_filter('wp_check_filetype_and_ext', function($data, $file, $filename, $mimes) {
$ext = pathinfo($filename, PATHINFO_EXTENSION);
if ($ext === 'avif') {
$data['ext'] = 'avif';
$data['type'] = 'image/avif';
}
return $data;
}, 10, 4);
WebP Conversion on Upload
// Auto-convert uploads to WebP (requires GD or Imagick)
add_filter('wp_generate_attachment_metadata', function($metadata, $attachment_id) {
$file = get_attached_file($attachment_id);
$info = pathinfo($file);
// Only process JPEG/PNG
if (!in_array(strtolower($info['extension']), ['jpg', 'jpeg', 'png'])) {
return $metadata;
}
// Generate WebP version
$webp_path = $info['dirname'] . '/' . $info['filename'] . '.webp';
if (extension_loaded('imagick')) {
$image = new Imagick($file);
$image->setImageFormat('webp');
$image->setImageCompressionQuality(80);
$image->writeImage($webp_path);
$image->destroy();
} elseif (function_exists('imagewebp')) {
$source = imagecreatefromstring(file_get_contents($file));
imagewebp($source, $webp_path, 80);
imagedestroy($source);
}
return $metadata;
}, 10, 2);
Responsive Images
WordPress Default Srcset
WordPress automatically adds srcset to content images:
<!-- WordPress output -->
<img
src="image-1024x768.jpg"
srcset="image-300x225.jpg 300w,
image-768x576.jpg 768w,
image-1024x768.jpg 1024w,
image-1536x1152.jpg 1536w"
sizes="(max-width: 1024px) 100vw, 1024px"
alt="Description"
>
Customizing Srcset
// Modify srcset sizes
add_filter('wp_calculate_image_srcset', function($sources, $size_array, $image_src, $image_meta, $attachment_id) {
// Remove unwanted sizes
foreach ($sources as $width => $source) {
if ($width > 2000) {
unset($sources[$width]);
}
}
return $sources;
}, 10, 5);
// Customize sizes attribute
add_filter('wp_calculate_image_sizes', function($sizes, $size, $image_src, $image_meta, $attachment_id) {
// Custom sizes for full-width images
if ($size[0] >= 1200) {
return '(max-width: 768px) 100vw, (max-width: 1200px) 80vw, 1200px';
}
return $sizes;
}, 10, 5);
Theme Implementation
// In theme template
$image_id = get_post_thumbnail_id();
echo wp_get_attachment_image($image_id, 'large', false, [
'class' => 'hero-image',
'loading' => 'eager',
'fetchpriority' => 'high',
'sizes' => '100vw'
]);
Lazy Loading
Native Lazy Loading
Since WordPress 5.5, lazy loading is automatic:
// Disable lazy loading for specific images
add_filter('wp_img_tag_add_loading_attr', function($value, $image, $context) {
// Disable for featured images in hero sections
if (strpos($image, 'hero-image') !== false) {
return false;
}
return $value;
}, 10, 3);
// Disable lazy loading for first N images
add_filter('wp_lazy_loading_enabled', function($default, $tag_name, $context) {
static $count = 0;
if ($tag_name === 'img') {
$count++;
if ($count <= 2) {
return false; // Don't lazy load first 2 images
}
}
return $default;
}, 10, 3);
Skip Lazy Loading for LCP
// Add fetchpriority to first image
add_filter('wp_content_img_tag', function($filtered_image, $context, $attachment_id) {
static $first_image = true;
if ($first_image && $context === 'the_content') {
$first_image = false;
$filtered_image = str_replace(
'loading="lazy"',
'fetchpriority="high"',
$filtered_image
);
}
return $filtered_image;
}, 10, 3);
Featured Images
Basic Featured Image
// In template
if (has_post_thumbnail()) {
the_post_thumbnail('large', [
'class' => 'featured-image',
'loading' => 'eager'
]);
}
Responsive Featured Image
function responsive_featured_image($post_id = null, $loading = 'lazy') {
$post_id = $post_id ?: get_the_ID();
$image_id = get_post_thumbnail_id($post_id);
if (!$image_id) return '';
$sizes = [
['width' => 400, 'media' => '(max-width: 480px)'],
['width' => 800, 'media' => '(max-width: 768px)'],
['width' => 1200, 'media' => '(max-width: 1200px)'],
['width' => 1600, 'media' => '(min-width: 1201px)'],
];
$sources = '';
foreach ($sizes as $size) {
$image = wp_get_attachment_image_src($image_id, [$size['width'], 0]);
if ($image) {
$sources .= sprintf(
'<source media="%s" srcset="%s">',
esc_attr($size['media']),
esc_url($image[0])
);
}
}
$default = wp_get_attachment_image_src($image_id, 'large');
$alt = get_post_meta($image_id, '_wp_attachment_image_alt', true);
return sprintf(
'<picture>%s<img src="%s" alt="%s" loading="%s" class="featured-image"></picture>',
$sources,
esc_url($default[0]),
esc_attr($alt),
esc_attr($loading)
);
}
Gutenberg Block Images
Image Block Enhancements
// Add custom attributes to image block
add_filter('render_block_core/image', function($block_content, $block) {
// Add loading optimization
if (strpos($block_content, 'loading=') === false) {
$block_content = str_replace(
'<img',
'<img loading="lazy" decoding="async"',
$block_content
);
}
return $block_content;
}, 10, 2);
Custom Image Block
// blocks/optimized-image/edit.js
import { useBlockProps, MediaUpload, MediaUploadCheck } from '@wordpress/block-editor';
import { Button, SelectControl } from '@wordpress/components';
export default function Edit({ attributes, setAttributes }) {
const { mediaId, mediaUrl, size, loading } = attributes;
const blockProps = useBlockProps();
return (
<div {...blockProps}>
<MediaUploadCheck>
<MediaUpload
onSelect={(media) => setAttributes({
mediaId: media.id,
mediaUrl: media.sizes[size]?.url || media.url
})}
allowedTypes={['image']}
value={mediaId}
render={({ open }) => (
<Button onClick={open}>
{mediaUrl ? (
<img src={mediaUrl} alt="" />
) : (
'Select Image'
)}
</Button>
)}
/>
</MediaUploadCheck>
<SelectControl
label="Loading"
value={loading}
options={[
{ label: 'Lazy', value: 'lazy' },
{ label: 'Eager', value: 'eager' }
]}
onChange={(value) => setAttributes({ loading: value })}
/>
</div>
);
}
WooCommerce Images
Product Image Sizes
// Customize WooCommerce image sizes
add_filter('woocommerce_get_image_size_single', function($size) {
return [
'width' => 800,
'height' => 800,
'crop' => 0
];
});
add_filter('woocommerce_get_image_size_thumbnail', function($size) {
return [
'width' => 400,
'height' => 400,
'crop' => 1
];
});
add_filter('woocommerce_get_image_size_gallery_thumbnail', function($size) {
return [
'width' => 150,
'height' => 150,
'crop' => 1
];
});
Product Gallery Optimization
// Optimize product gallery loading
add_action('woocommerce_before_single_product_summary', function() {
// Preload main product image
global $product;
$image_id = $product->get_image_id();
if ($image_id) {
$image_url = wp_get_attachment_image_url($image_id, 'woocommerce_single');
echo '<link rel="preload" as="image" href="' . esc_url($image_url) . '">';
}
}, 1);
// Lazy load gallery thumbnails
add_filter('woocommerce_single_product_image_gallery_classes', function($classes) {
$classes[] = 'gallery-lazy';
return $classes;
});
Category Page Optimization
// Optimize category thumbnails
add_action('woocommerce_before_shop_loop_item_title', function() {
global $product;
static $count = 0;
$count++;
// Eager load first row (assuming 4 per row)
$loading = $count <= 4 ? 'eager' : 'lazy';
echo '<div class="product-image-wrapper">';
echo wp_get_attachment_image(
$product->get_image_id(),
'woocommerce_thumbnail',
false,
[
'loading' => $loading,
'class' => 'product-thumbnail'
]
);
echo '</div>';
}, 9);
// Remove default thumbnail
remove_action('woocommerce_before_shop_loop_item_title', 'woocommerce_template_loop_product_thumbnail', 10);
Image Optimization Plugins
Sirv Plugin (Recommended)
The Sirv WordPress plugin provides the most complete image CDN and optimization solution for WordPress and WooCommerce:
Key Features:
- Automatic optimization of all images (no manual work)
- Automatic scaling to exact dimensions needed
- Automatic AVIF & WebP delivery (smaller than any other plugin)
- Fast global CDN (25+ locations worldwide)
- Deep image zoom for product photography
- 360-degree product spins
- 3D model support (GLB/USDZ with AR)
- Video streaming with adaptive bitrate
- Lazy loading built-in
What Gets Optimized:
- Media library images
- Featured images
- WooCommerce product & category images
- CSS background images
- Images from other plugins
Installation:
- Install the Sirv plugin from WordPress.org
- Connect your Sirv account
- Enable CDN - images sync automatically
// Sirv handles everything automatically, but you can also use the API
function get_sirv_optimized_url($attachment_id, $width = 800) {
$url = wp_get_attachment_url($attachment_id);
// Sirv plugin automatically rewrites URLs to CDN
return $url;
}
Pricing: Free plan (500MB storage, 2GB/month transfer), paid plans from $19/month for 5GB storage.
Other Plugins
| Plugin | Features | Best For |
|---|---|---|
| ShortPixel | Lossy/lossless, WebP, CDN | General optimization |
| Imagify | Bulk optimize, WebP, backup | Agencies |
| Smush | Free tier, lazy load | Budget-conscious |
| EWWW | Local processing, WebP | Privacy-focused |
| Optimole | CDN, real-time, adaptive | High traffic |
Plugin Configuration (ShortPixel Example)
// Programmatic configuration
add_filter('shortpixel_image_optimiser_settings', function($settings) {
return array_merge($settings, [
'api_key' => SHORTPIXEL_API_KEY,
'compression_type' => 1, // Lossy
'create_webp' => true,
'create_avif' => true,
'resize_images' => true,
'resize_width' => 2560,
'resize_height' => 2560
]);
});
WebP Delivery Without Plugin
# .htaccess WebP rewrite
<IfModule mod_rewrite.c>
RewriteEngine On
RewriteCond %{HTTP_ACCEPT} image/webp
RewriteCond %{REQUEST_FILENAME} (.*)\.(jpe?g|png)$
RewriteCond %{REQUEST_FILENAME}\.webp -f
RewriteRule (.+)\.(jpe?g|png)$ %{REQUEST_FILENAME}.webp [T=image/webp,E=accept:1]
</IfModule>
<IfModule mod_headers.c>
Header append Vary Accept env=REDIRECT_accept
</IfModule>
CDN Integration
Rewriting Image URLs
// Replace uploads URL with CDN
add_filter('wp_get_attachment_url', function($url) {
$upload_url = wp_upload_dir()['baseurl'];
$cdn_url = 'https://cdn.example.com/wp-content/uploads';
return str_replace($upload_url, $cdn_url, $url);
});
// For srcset too
add_filter('wp_calculate_image_srcset', function($sources) {
$upload_url = wp_upload_dir()['baseurl'];
$cdn_url = 'https://cdn.example.com/wp-content/uploads';
foreach ($sources as $width => $source) {
$sources[$width]['url'] = str_replace($upload_url, $cdn_url, $source['url']);
}
return $sources;
});
Sirv CDN Integration
The easiest way to integrate Sirv is with the official WordPress plugin, which handles everything automatically. For custom implementations:
// Manual Sirv image transformation (if not using plugin)
function sirv_image_url($attachment_id, $args = []) {
$defaults = [
'width' => 800,
'quality' => 80,
'format' => 'optimal'
];
$args = wp_parse_args($args, $defaults);
$url = wp_get_attachment_url($attachment_id);
// Build Sirv URL with dynamic imaging parameters
$sirv_url = sprintf(
'https://yoursite.sirv.com%s?w=%d&q=%d&format=%s',
parse_url($url, PHP_URL_PATH),
$args['width'],
$args['quality'],
$args['format']
);
return $sirv_url;
}
// With the Sirv plugin installed, URLs are rewritten automatically
// and you get AVIF/WebP, lazy loading, and responsive images for free
Performance Optimization
Preloading Critical Images
// Preload hero images
add_action('wp_head', function() {
if (is_front_page() && has_post_thumbnail()) {
$image_id = get_post_thumbnail_id();
$image = wp_get_attachment_image_src($image_id, 'full');
if ($image) {
printf(
'<link rel="preload" as="image" href="%s" imagesrcset="%s" imagesizes="100vw">',
esc_url($image[0]),
esc_attr(wp_get_attachment_image_srcset($image_id, 'full')),
);
}
}
}, 1);
Image Compression on Upload
// Compress JPEG on upload using GD
add_filter('wp_handle_upload', function($upload) {
$path = $upload['file'];
$type = $upload['type'];
if (strpos($type, 'image/jpeg') !== false) {
$image = imagecreatefromjpeg($path);
if ($image) {
imagejpeg($image, $path, 85);
imagedestroy($image);
}
}
return $upload;
});
Limiting Image Sizes
// Remove unused default sizes
add_filter('intermediate_image_sizes_advanced', function($sizes) {
// Remove sizes you don't need
unset($sizes['medium_large']); // 768px
unset($sizes['1536x1536']);
unset($sizes['2048x2048']);
return $sizes;
});
// Limit maximum upload size
add_filter('wp_handle_upload_prefilter', function($file) {
$max_width = 2560;
$max_height = 2560;
$image = getimagesize($file['tmp_name']);
if ($image) {
if ($image[0] > $max_width || $image[1] > $max_height) {
$file['error'] = sprintf(
'Image dimensions (%dx%d) exceed maximum allowed (%dx%d)',
$image[0], $image[1], $max_width, $max_height
);
}
}
return $file;
});
Theme Development
Theme Image Functions
// functions.php
/**
* Get optimized image HTML
*/
function theme_get_image($attachment_id, $size = 'large', $args = []) {
$defaults = [
'loading' => 'lazy',
'class' => '',
'sizes' => '100vw'
];
$args = wp_parse_args($args, $defaults);
return wp_get_attachment_image($attachment_id, $size, false, [
'loading' => $args['loading'],
'class' => $args['class'],
'sizes' => $args['sizes'],
'decoding' => 'async'
]);
}
/**
* Get responsive picture element
*/
function theme_get_picture($attachment_id, $args = []) {
$defaults = [
'mobile_size' => 'medium',
'desktop_size' => 'large',
'breakpoint' => 768,
'loading' => 'lazy'
];
$args = wp_parse_args($args, $defaults);
$mobile = wp_get_attachment_image_src($attachment_id, $args['mobile_size']);
$desktop = wp_get_attachment_image_src($attachment_id, $args['desktop_size']);
$alt = get_post_meta($attachment_id, '_wp_attachment_image_alt', true);
ob_start();
?>
<picture>
<source media="(min-width: <?php echo $args['breakpoint']; ?>px)"
srcset="<?php echo esc_url($desktop[0]); ?>">
<img src="<?php echo esc_url($mobile[0]); ?>"
alt="<?php echo esc_attr($alt); ?>"
loading="<?php echo esc_attr($args['loading']); ?>"
decoding="async">
</picture>
<?php
return ob_get_clean();
}
ACF Image Fields
// Display ACF image field optimized
function display_acf_image($field_name, $post_id = null) {
$image = get_field($field_name, $post_id);
if (!$image) return '';
// If returning array
if (is_array($image)) {
return wp_get_attachment_image($image['ID'], 'large', false, [
'loading' => 'lazy',
'sizes' => '(max-width: 768px) 100vw, 50vw'
]);
}
// If returning ID
if (is_numeric($image)) {
return wp_get_attachment_image($image, 'large', false, [
'loading' => 'lazy'
]);
}
// If returning URL (not recommended)
return sprintf('<img src="%s" loading="lazy">', esc_url($image));
}
Media Library Management
Cleanup Unused Images
// Find unused images
function find_unused_images() {
global $wpdb;
$attachments = get_posts([
'post_type' => 'attachment',
'post_mime_type' => 'image',
'posts_per_page' => -1,
'fields' => 'ids'
]);
$unused = [];
foreach ($attachments as $id) {
$url = wp_get_attachment_url($id);
$filename = basename($url);
// Check if used in content
$used = $wpdb->get_var($wpdb->prepare(
"SELECT ID FROM {$wpdb->posts}
WHERE post_content LIKE %s
AND post_status = 'publish'
LIMIT 1",
'%' . $wpdb->esc_like($filename) . '%'
));
// Check if used as featured image
$featured = $wpdb->get_var($wpdb->prepare(
"SELECT meta_id FROM {$wpdb->postmeta}
WHERE meta_key = '_thumbnail_id'
AND meta_value = %d
LIMIT 1",
$id
));
if (!$used && !$featured) {
$unused[] = $id;
}
}
return $unused;
}
Regenerate Thumbnails Programmatically
// Regenerate specific image
function regenerate_image_sizes($attachment_id) {
$file = get_attached_file($attachment_id);
if (!file_exists($file)) {
return new WP_Error('file_not_found', 'Original file not found');
}
// Generate new metadata
$metadata = wp_generate_attachment_metadata($attachment_id, $file);
// Update metadata
wp_update_attachment_metadata($attachment_id, $metadata);
return $metadata;
}
Troubleshooting
Common Issues
Missing srcset:
// Ensure image editor is available
add_filter('wp_image_editors', function($editors) {
// Prefer Imagick for better quality
return ['WP_Image_Editor_Imagick', 'WP_Image_Editor_GD'];
});
Wrong image sizes:
// Clear image metadata cache
delete_post_meta($attachment_id, '_wp_attachment_metadata');
wp_update_attachment_metadata(
$attachment_id,
wp_generate_attachment_metadata($attachment_id, get_attached_file($attachment_id))
);
Slow uploads:
// Increase memory for image processing
add_filter('wp_max_upload_size', function() {
return 50 * 1024 * 1024; // 50MB
});
// Configure PHP settings in php.ini or wp-config.php
// memory_limit = 256M
// max_execution_time = 300
Summary
Configuration Checklist
// Complete optimization setup
add_action('after_setup_theme', function() {
// Custom image sizes
add_image_size('hero', 1920, 800, true);
add_image_size('card', 600, 400, true);
});
// Remove unused sizes
add_filter('intermediate_image_sizes_advanced', function($sizes) {
unset($sizes['1536x1536']);
unset($sizes['2048x2048']);
return $sizes;
});
// Optimize loading
add_filter('wp_lazy_loading_enabled', '__return_true');
// Enable WebP
add_filter('upload_mimes', function($mimes) {
$mimes['webp'] = 'image/webp';
return $mimes;
});
Quick Reference
| Task | Code |
|---|---|
| Get responsive image | wp_get_attachment_image($id, 'large') |
| Featured image | the_post_thumbnail('large', ['loading' => 'eager']) |
| Disable lazy load | ['loading' => 'eager', 'fetchpriority' => 'high'] |
| Custom srcset | wp_get_attachment_image_srcset($id, 'large') |
Performance Checklist
- ✅ Use optimized image sizes (remove unused)
- ✅ Enable WebP/AVIF conversion
- ✅ Implement lazy loading (skip above-fold)
- ✅ Add fetchpriority to LCP images
- ✅ Preload hero/critical images
- ✅ Use CDN for image delivery
- ✅ Compress images on upload
- ✅ Generate proper srcset for responsive
- ✅ Set explicit width/height attributes
- ✅ Limit maximum upload dimensions
- ✅ Clean up unused media regularly
- ✅ Test with Page Speed Insights
WordPress provides strong image optimization foundations. Combine core features with targeted plugins and proper theme implementation for optimal image performance.