Since WordPress 5.5 (2020) core automatically attaches loading="lazy" to every image inside post content. The browser uses it to defer downloading images outside the initial viewport - a major reduction in initial page weight. Plenty of plugins, themes, and page builders break the behavior, however: they call the_post_thumbnail() outside core helpers, force loading="eager" on every image, or hand-build the markup as a raw string.
Why this matters
On a 30-image page that scrolls long, only 3-5 images appear above the fold. Loading all 30 immediately means 5MB+ on first paint. With lazy loading, the first 500KB suffice and the rest stream in as the user scrolls. That maps directly to 30-50% faster LCP, lower Total Blocking Time, and higher Lighthouse scores.
Critical caveat: the first image in the viewport (the LCP candidate) must be eager, not lazy. Lazy on the LCP element forces the browser to wait until it confirms viewport intersection - that adds 100-300ms to LCP. WordPress 5.9+ tries to detect the LCP candidate and mark it fetchpriority="high" automatically, but it does not always succeed.
How to detect
Easy version: view source on the homepage, search for <img, and count how many include loading="lazy". Below 70% means trouble.
Cleaner version: F12 > Console, run:
const imgs = document.querySelectorAll('img');
const lazy = [...imgs].filter(i => i.loading === 'lazy').length;
console.log(`${lazy}/${imgs.length} (${Math.round(lazy/imgs.length*100)}%)`);LCP-specific check: F12 > Lighthouse > Performance > Run audit. Under "Largest Contentful Paint element" look at the offending image - confirm it is not lazy.
How to fix
Page builders first: in Elementor (Settings > Performance > Lazy Load Images = ON), Divi (Theme Options > General > Performance), or Bricks (Settings > Performance) - enable lazy.
Check functions.php and any plugin for a global kill switch:
// remove this if you find it
add_filter('wp_lazy_loading_enabled', '__return_false');For custom theme code, prefer core helpers - they apply the filter chain that adds lazy:
// good - flows through filters, picks up lazy automatically
echo get_the_post_thumbnail($post_id, 'large');
// or:
echo wp_get_attachment_image($attachment_id, 'large');Avoid hand-rolled <img src="..." /> markup without explicit lazy.
For the LCP candidate (hero image), force eager:
<img src="hero.jpg" loading="eager" fetchpriority="high" alt="..." />For iframes and CSS background images, install a3 Lazy Load or FlyingPress - core only handles <img>.
Common mistakes
Mistake one: lazy-loading the first viewport image. LCP gets delayed every single time. Always pair the hero/cover with eager + fetchpriority=high. Mistake two: stacking a third-party lazy plugin on top of native lazy. The two mechanisms collide; one cancels the other on certain images. Pick one. Mistake three: relying on Cloudflare Mirage or Polish alone - they operate at the network layer and do not patch every image attribute reliably.
Verifying the fix
Re-run the Console snippet - 90%+ of images should be lazy (the first one or two stay eager). PageSpeed Insights LCP should fall 200-500ms. In DevTools Network, reload and confirm images near the bottom of the page do not appear in the initial request waterfall - only after scrolling. WebPageTest Visual Progress should show the top of the page rendering faster.