What WordPress does with an upload
When an image is uploaded, WordPress keeps the original and generates a set of resized copies: thumbnail, medium, medium_large, large, and, since 5.3, scaled versions at 1536 and 2048 pixels, plus a -scaled original if the upload exceeds 2560. Themes and plugins register more. When the image is placed in content, WordPress outputs an <img> with srcset listing those sizes and a sizes attribute so the browser picks the smallest adequate one.
Since 5.5 it adds loading="lazy" to images, and since 5.9 it skips lazy loading on the first image in content. Since 6.1 it can generate WebP for JPEG uploads, though that default was made opt-in; since 6.3 it adds fetchpriority="high" to the image it guesses is the largest above the fold.
That is a reasonable system. It goes wrong in predictable places.
Where the defaults hurt
The sizes attribute is wrong. WordPress defaults to sizes="(max-width: {width}px) 100vw, {width}px", which assumes the image is full-viewport width. In a 400-pixel column, the browser is told the image is as wide as the screen and downloads the 1024-pixel version for a 400-pixel slot. Themes must filter wp_calculate_image_sizes or set sizes per context. Most do not. Block themes with theme.json layout widths do better here, and 6.x versions improved the default computation, but check.
Every image is lazy-loaded, including the hero. The first image in the_content is exempted, but the hero is usually in the template, the header or a block outside the content, so it gets loading="lazy" and waits for layout before it even starts downloading. That is a direct hit to Largest Contentful Paint.
The LCP image is a CSS background. Page builders and many themes render the hero as background-image. The browser cannot discover it until the CSS is parsed and the element is laid out, so it starts late and there is no srcset, so phones get the desktop file.
Originals are enormous. A 6000-pixel phone photo becomes a 2560-pixel -scaled file, still several hundred kilobytes, and a theme that outputs the "full" size gets it.
No modern format. JPEG and PNG when WebP or AVIF would be a third to a half the size at the same quality.
Too many sizes. Themes and plugins registering a dozen image sizes means a dozen files generated per upload, filling the disk and slowing uploads, most never used.
The configuration
Get the LCP image right
Identify the LCP element on the key pages (DevTools Performance panel names it). For each:
- Render it as an
<img>, not a background. Use a cover-fit image withobject-fit: coverif the design needs a background look. - Give it
fetchpriority="high"and noloading="lazy". WordPress's automatic detection helps; make it explicit for the hero in the template viawp_get_attachment_imagewith the attributes set, or thewp_get_attachment_image_attributesfilter for a known context. - Preload it for the page where it is known at template time:
<link rel="preload" as="image" href="..." imagesrcset="..." imagesizes="...">viawp_head. Use thesrcsetform so the phone preloads the phone size. - Make sure its
sizesattribute is truthful, so a phone gets a phone-sized file.
That set of changes is often the difference between an LCP of 4 seconds and 1.5 on a WordPress site.
Fix sizes globally
Filter wp_calculate_image_sizes to return correct values for your layout's column widths, or use a theme that sets them from theme.json layout settings. For content images in a constrained column, something like (max-width: 768px) 100vw, 720px is closer to the truth than the default.
Serve modern formats
Enable WebP generation for uploads (add_filter('image_editor_output_format', ...) mapping image/jpeg to image/webp), or let an image optimisation plugin or the host's CDN convert and serve WebP or AVIF with content negotiation. The CDN route is the least invasive: originals stay as uploaded and the edge serves the best format each browser supports.
Compress on upload
A cap on upload dimensions (2560 is WordPress's default for the scaled copy; 2000 is enough for most sites) and quality around 80 for JPEG/WebP. An optimisation plugin or the host's pipeline handles this; the point is that the client's raw phone photo never becomes the served file.
Prune image sizes
List registered sizes with wp_get_registered_image_subsizes() and remove the ones nothing uses via remove_image_size or the intermediate_image_sizes_advanced filter. Regenerate thumbnails once after pruning if disk space matters.
Keep lazy loading, below the fold
For everything that is not the LCP image and not in the first viewport, loading="lazy" is right and free. Check that any slider or gallery plugin is not overriding it with its own JavaScript lazy loader that defers images the browser would have handled better natively.
Dimensions always
Every <img> needs width and height (or CSS aspect-ratio) so the browser reserves space and nothing shifts when it loads. WordPress adds them for attachments; check images inserted by plugins and in hand-written templates.
Verifying
Lighthouse on mobile before and after, looking at the LCP element and the "Properly size images" and "Serve images in next-gen formats" audits. Then the real-user Core Web Vitals report after a month. On sites where images were the problem, this work alone moves pages from Poor to Good.
Where this fits
Image handling is the first thing we check on any WordPress performance job and the most common single cause of a failing LCP. It is part of every WordPress build, where the hero is set up correctly from the start, and one of the items kept in order on a care plan, because the client's next upload is always about to undo it.