Images are usually the heaviest thing on a WordPress page, and they are also the lever that moves PageSpeed scores the most for the least effort. The mechanism is simple: bytes shipped to the browser cost time, and the largest single byte payload on most WordPress pages is one of the images in the layout. That hero photo at the top of a homepage is also, in most themes, the Largest Contentful Paint element. Get the format, the compression, and the loading attributes right on that one image and the score moves. Get them wrong and nothing else you do on the page matters quite as much.
Why images are the biggest WordPress performance lever
Three separate mechanisms decide how much an image costs the browser. The first is byte size: a 2 MB JPEG straight off a camera is roughly twenty times the byte payload of the same picture saved as a properly compressed WebP at quality 80. The second is render blocking: a hero image that the browser does not know is the LCP candidate gets queued behind other resources, which delays first paint. The third is layout shift: an image without width and height attributes lands on the page after the surrounding text has flowed, the browser reflows everything, and the visitor sees a jump.
WordPress core has been picking up small fixes for all three of these for years. Responsive image sizing via srcset and sizes arrived in 4.4. Native lazy loading on <img> arrived in 5.5. WebP upload acceptance arrived in 5.8. Native fetchpriority for the LCP candidate arrived in 6.3. AVIF upload acceptance arrived in 6.5. None of these is a complete solution on its own, but together they mean a default WordPress 6.5 install does most of the right things automatically as long as you upload sensibly sized files in a sensible format and do not actively break the defaults.
For a broader framing of what "slow" actually means and where image work fits into the bigger picture, see why a WordPress site feels slow.
WebP vs JPEG vs PNG vs AVIF: which format and when
The format choice maps fairly cleanly to the use case. AVIF gives the best compression of the four, roughly 50% smaller than a JPEG at equivalent perceived quality, and is supported in every modern browser as of 2024. WebP gives roughly 25 to 35% better compression than JPEG and has been universally supported in browsers since Safari 14. JPEG is still the safe baseline for photographs when you cannot guarantee server-side support for the newer formats. PNG is only the right answer for graphics with hard transparency where neither WebP nor AVIF is an option, which in 2026 is a small set of cases.
In practice, most WordPress sites should standardise on WebP for photographs and reach for AVIF when the host supports it and the page weight matters enough to justify the second format. The reason is not technical superiority, it is operational simplicity. Serving WebP only is one format and one fallback. Serving AVIF properly means generating AVIF, WebP, and JPEG, and wrapping them in a <picture> element with <source> tags so that browsers without AVIF support fall back cleanly. WordPress core does not generate <picture> elements out of the box. If you want format negotiation via <picture>, you need a plugin (Modern Image Formats from the Performance Lab team, or any of the heavyweight optimisation plugins) or a CDN that does it for you.
A useful rule of thumb: if the site is on a managed host with Imagick and libavif, and the homepage carries a hero photo bigger than 200 KB, AVIF is worth the extra plugin. If the site is on shared hosting with LibGD only, stick with WebP and a single fallback to JPEG.
WordPress 5.8 native WebP is upload acceptance, not auto-conversion
This is the point most articles get wrong, so let me be precise. WordPress 5.8 added WebP to the list of upload-allowed MIME types. That is what "native WebP support" means in the WordPress changelog. It means you can drag a .webp file into the Media Library and WordPress will accept it, generate the usual sub-sizes, include them in srcset, and treat the file as a first-class image.
What 5.8 does not do, and what no version of WordPress core does today, is automatically convert your existing JPEG and PNG uploads to WebP. If you upload holiday.jpg, WordPress stores holiday.jpg. It does not also generate holiday.webp. It does not wrap the output in a <picture> element with a WebP source. The bytes the browser downloads are exactly the JPEG bytes you uploaded.
There are three ways to bridge that gap. The first is to convert images to WebP yourself before uploading, using Squoosh, sharp, or any image editor that exports WebP at quality 75 to 85. The second is to install the Modern Image Formats plugin (formerly the WebP Uploads module from the WordPress Performance Group), which auto-generates WebP versions on upload and rewrites the output to use them. The third is a third-party optimisation plugin like ShortPixel, Imagify, or EWWW Image Optimizer, which does the same thing plus a few extras like batch conversion of existing media library entries.
There is also a fourth option for developers who want to keep the plugin count low: a one-line filter that tells WordPress to use WebP as the output format for all sub-size generation.
// Tell WordPress to generate WebP sub-sizes for JPEG uploads.
// WordPress 6.5+.
add_filter( 'image_editor_output_format', function( $formats ) {
$formats['image/jpeg'] = 'image/webp';
return $formats;
} );
This filter only affects newly uploaded images and only the resized sub-sizes, not the original. The original JPEG is still stored. It is the lightest possible WebP on-ramp and a reasonable starting point if you do not want to add a plugin just for format conversion.
WordPress 6.5 added AVIF support
WordPress 6.5, released in April 2024, added AVIF to the upload-allowed MIME types and wired it into the same image-handling pipeline as WebP. The same scope rules apply. Core accepts AVIF uploads, it generates AVIF sub-sizes if the server's Imagick or LibGD installation supports the format, and it includes AVIF files in srcset. It does not auto-convert existing JPEG or PNG uploads to AVIF, and it does not wrap output in <picture> elements with AVIF sources.
The server requirement is the gotcha. AVIF support in PHP depends on the underlying image library having been compiled with libavif, which is not universal. To check whether your host supports AVIF, go to Tools, Site Health, Info, Media Handling and look for AVIF in the supported image formats list. If it is not there, AVIF uploads will be rejected and the rest of this section does not apply.
For sites that do have AVIF support, the same three upgrade paths from the WebP section apply: convert before upload, install Modern Image Formats from Performance Lab, or use a third-party optimisation plugin that handles AVIF generation.
Compressing images at upload time
Compression is where most of the byte savings live. A photograph from a phone camera is typically a 2 to 5 MB JPEG straight out of the camera and a 200 to 400 KB WebP at quality 80, which is the same picture as far as a visitor is concerned. The two compress-before-upload options that work well for non-developers are Squoosh for one-off images in a browser tab, and a plugin that compresses at upload time for the steady stream of editorial uploads.
The plugin landscape is roughly:
- ShortPixel. Cloud-based, pay-per-image after a free monthly allowance. Generates WebP and AVIF. Replaces or keeps originals on request. Solid choice for sites with high upload volume.
- Imagify. Cloud-based, similar pricing model to ShortPixel. From the WP Rocket team.
- EWWW Image Optimizer. Server-based by default (uses local binaries), with an optional cloud tier. Useful when you do not want uploads leaving the server.
- Smush. Server-based with a free tier. The free tier is lossless only, which is rarely the right choice for photographs.
- Performance Lab + Modern Image Formats. From the WordPress Performance Group. Free, official, focused. Handles WebP and AVIF generation but not aggressive compression on JPEG itself, so it pairs well with manual compression of originals.
The compression number that matters most is the quality setting. Quality 75 to 85 is the visually indistinguishable range for photographs at screen resolutions. Below 75 you start to see banding in skies and softness in detail. Above 85 the file size grows quickly without a visible gain. Whatever plugin you pick, set its quality to 80 unless you have a reason not to.
A common misconception worth flagging: "uploading smaller images always looks bad". It does not. The thing that looks bad is uploading at the wrong dimensions for the display container. A 3000 pixel wide image displayed at 400 pixels wide is wasted bandwidth regardless of how aggressively you compress it. The better lever than compression in many cases is uploading at a sensible source resolution and letting WordPress generate the sub-sizes from there. Twice the display width is a reasonable target for retina displays.
Lazy loading: WordPress 5.5 default for img, 5.7 default for iframe
WordPress 5.5 added the loading="lazy" attribute to image tags by default. The attribute tells the browser to defer downloading the image until it is close to entering the viewport, which on a long page can save megabytes of bytes the visitor never scrolls to. WordPress 5.5 only added this for <img> elements. WordPress 5.7 extended it to <iframe> elements so that embedded YouTube videos and similar iframes get the same treatment.
Browser support is now near-universal: Chrome 77 and later, Edge 79 and later, Firefox 75 and later, Safari 15.4 and later. Browsers without support simply ignore the attribute and load the image normally, so adding it is safe.
Important nuance: lazy loading every image does not improve performance. It actively harms it for above-the-fold images, which is the next section.
The LCP image exception
The single most damaging mistake in WordPress image performance is lazy-loading the LCP image. When the browser sees loading="lazy" on an image, it deprioritises the fetch and waits to see if the image enters the viewport. If that image is the LCP element, the metric measures from the start of the navigation until the image renders, and the lazy attribute pushes the render later by hundreds of milliseconds or more. PageSpeed Insights flags this explicitly with the diagnostic "Largest Contentful Paint image was lazily loaded".
WordPress 5.9 partially fixed this by excluding the first content image from automatic lazy loading. WordPress 6.3 went further and introduced wp_get_loading_optimization_attributes(), which not only skips lazy loading on the likely LCP image but also adds fetchpriority="high" to it. The function applies to images rendered through wp_get_attachment_image(), get_avatar(), get_header_image_tag(), and the other core image helpers, and it triggers when the image is large enough to be a plausible LCP candidate (the default minimum is 50,000 square pixels, configurable via the wp_min_priority_img_pixels filter).
The catch is that "the first content image" only works if WordPress can identify it. Images output by themes or page builders that render their own <img> tags rather than using wp_get_attachment_image() bypass the optimization. If your hero image is rendered by a custom theme template using a hand-written <img> tag, you have to set the attributes yourself.
// Force loading=eager and fetchpriority=high on a specific image size.
// Useful when a theme uses wp_get_attachment_image() but you want to be
// explicit about the hero treatment.
// WordPress 6.5+.
add_filter( 'wp_get_attachment_image_attributes', function( $attr, $attachment, $size ) {
if ( $size === 'hero-banner' ) {
$attr['loading'] = 'eager';
$attr['fetchpriority'] = 'high';
}
return $attr;
}, 10, 3 );
If your theme outputs raw <img> tags for the hero, the fix is in the theme template itself. Add loading="eager" and fetchpriority="high" directly to the <img> tag and remove any loading="lazy" your code or a plugin previously added.
A final detail from Google's web.dev guidance on lazy loading: never combine loading="lazy" with fetchpriority="high" on the same image. They contradict each other. The browser already prioritises images that are about to enter the viewport, so the combination has no benefit and signals to the browser that you do not actually know which image is the LCP candidate.
Always set width and height attributes
This is the smallest fix on the list and it stops one of the most visible PageSpeed warnings. Every <img> should have explicit width and height attributes. The values are the intrinsic pixel dimensions of the source file. The browser uses them to reserve space in the layout before the image has loaded, which prevents the layout shift the visitor sees as the page jumping around once images arrive.
wp_get_attachment_image() adds these attributes automatically. Theme code that builds <img> tags by hand often does not. If you maintain a theme, add the attributes. If you cannot edit the theme, the wp_img_tag_add_width_and_height_attr filter and the wp_filter_content_tags pipeline that calls it will add them to images in post content for you, and it has been on by default since 5.5.
Regenerating thumbnails after changing image sizes
If you change the registered image sizes in your theme, or you switch themes and the new theme uses a different medium_large width, existing uploads still have the old sub-sizes on disk. New uploads will use the new sizes, but the back catalogue is stale. There is a Regenerate Thumbnails plugin for the GUI route, and a wp media regenerate command for WP-CLI.
# Regenerate all sub-sizes for every attachment.
# Use --yes to skip the confirmation prompt on a large media library.
# WP-CLI 2.10+, WordPress 6.5+.
wp media regenerate --yes
Run this once after changing theme image sizes, after switching to a new theme, or after installing a plugin that adds new image sizes (most cache plugins, AMP, and the major page builders all do).
Serving images via CDN
A CDN moves the image bytes physically closer to the visitor. For a Dutch audience hitting a server in Amsterdam, that gain is small. For a global audience, it can shave hundreds of milliseconds off image fetch time. For sites where image performance is the bottleneck and the audience is geographically spread, an image-aware CDN like Cloudflare Images, BunnyCDN, KeyCDN, or Imgix is worth considering. They typically combine the CDN with on-the-fly format negotiation: the CDN serves AVIF to browsers that accept it, WebP to the rest, and JPEG as the final fallback, without you generating multiple files yourself.
The trade-off is one more moving part in the stack and one more bill. For a single-region WordPress site under 50,000 monthly visitors, a properly configured WebP setup served from the origin is usually fast enough that a CDN is not the highest-leverage next step. For more on that decision, see the Cloudflare configuration article.
When an optimisation plugin is not the whole answer
A common misconception worth correcting: "an optimisation plugin handles everything". It does not. The things plugins handle well are format conversion, compression, sub-size generation, and <picture> element output. The things plugins do not handle are missing width and height attributes in custom theme templates, incorrectly eager-loaded LCP images that are not actually the LCP element, and themes that load enormous decorative background images via CSS where lazy loading does not apply at all. The plugin is a useful tool. It is not a substitute for understanding what the page is actually shipping.
If your PageSpeed score is still bad after installing an optimisation plugin, run the page through PageSpeed Insights and read the Opportunities and Diagnostics sections. The diagnostics will tell you specifically which image is the LCP, whether it was lazy-loaded, what its actual byte size was, and what the savings would be at a different format or quality. That report is the most useful single piece of feedback you can get and it is free.
What to actually do, in order
- Audit the homepage and a typical content page in PageSpeed Insights and note which image is the LCP and how big it is.
- If the site is on WordPress 6.5 or later and the host supports it, install Modern Image Formats from Performance Lab and let it generate WebP (and AVIF if available) on new uploads.
- Set the optimisation plugin's quality to 80 for photographs.
- For the hero image specifically, confirm
loading="eager"andfetchpriority="high"are present in the rendered HTML. If not, add them via the theme template or thewp_get_attachment_image_attributesfilter. - Confirm every
<img>has explicitwidthandheight. - Regenerate thumbnails once after the plugin is installed so existing uploads pick up the new format.
- Re-run PageSpeed Insights and verify the LCP image is no longer flagged.
Do these in order and the score will move. The work is mostly mechanical once you know which image is the LCP and which version of WordPress you are on.