MaxtDesign

Lazy Loading Images the Right Way

Lazy loading WordPress images correctly — native loading=lazy, what NOT to lazy-load, the LCP image preload pattern, and the gotchas that hurt rather than help.

7 min readlazy loading,images,WordPress,LCP,performance,Core Web Vitals
M
MaxtDesign
Engineering

Lazy loading is one of the highest-leverage performance improvements you can make on a WordPress site — and one of the easiest to do wrong. Done correctly, your page-weight on first paint drops 60-80%, LCP improves, and bandwidth costs shrink. Done incorrectly, you delay the image users came to see and tank the metric you were trying to fix. This is the short, opinionated guide to doing it right.

What lazy loading actually is

Lazy loading defers downloading an image until it's about to enter the viewport. The browser keeps the image element in the DOM but doesn't fetch the bytes until the user scrolls close enough that they'll see it. For a long-form article with 30 inline images, that means 25+ images that never get fetched on the initial visit.

Modern browsers support this natively via the loading="lazy"attribute. It's in every browser worth supporting in 2026. JavaScript-based lazy-loading libraries (lazysizes, lozad.js) were necessary in the IE/Safari-quirks era; they're not necessary now and their JS overhead actively hurts.

The native attribute

<img src="/wp-content/uploads/2026/04/photo.webp"
     alt="A descriptive alt"
     width="1200"
     height="800"
     loading="lazy" />

Three details that make this work and that people skip:

  • Always include width and height. The browser reserves space for the image before it loads. Skip this and you get layout shift (CLS) when the image lands. Aspect-ratio in CSS works too, but explicit dimensions on the img element is more universal.
  • Always include alt text. Lazy loading and accessibility are independent — neither replaces the other.
  • Use the correct image format for the asset. Lazy loading a 4MB unoptimized PNG hides the cost from the initial load, but users who scroll still pay for it. Lazy loading is a complement to optimization, not a substitute.

WordPress's built-in behaviour

Since WordPress 5.5 (2020), WordPress automatically adds loading="lazy"to images in post content. As of 6.4 it also adds it to template images. You don't need a plugin for the basic case — the platform does it.

What WordPress does NOT do automatically:

  • Skip the LCP (above-the-fold) image — it lazy-loads everything by default
  • Add loading="lazy" to images output via custom theme code that bypasses wp_filter_content_tags()
  • Apply to background images set via CSS (lazy loading doesn't work on those at all without JS tricks)

The first one — lazy-loading the LCP image — is the most common cause of WordPress sites that regress after enabling lazy loading.

The LCP image must NOT be lazy-loaded

The Largest Contentful Paint image — typically the hero on a landing page or the featured image on a blog post — is the metric Google measures. Lazy-loading it tells the browser "you can wait on this", which delays LCP and tanks the score.

The fix: explicitly set loading="eager" on the LCP image, and ideally also preload it. WordPress 6.3+ tries to detect the LCP image automatically via wp_omit_loading_attr_threshold (the first N images skip the lazy attribute). This is heuristic and gets it wrong on sites with custom layouts.

For featured images on single-post layouts, override explicitly:

add_filter( 'wp_get_attachment_image_attributes', 'mxt_eager_load_featured', 10, 3 );

function mxt_eager_load_featured( $attr, $attachment, $size ) {
    if ( is_singular() && has_post_thumbnail() &&
         $attachment->ID === get_post_thumbnail_id() ) {
        $attr['loading']      = 'eager';
        $attr['fetchpriority'] = 'high';
    }
    return $attr;
}

The fetchpriority="high" attribute is the 2024+ improvement on top of loading. It tells the browser to prioritize this resource above others — measurably improves LCP on hero images.

Preloading the LCP image

Even better than eager loading: preload the LCP image so it starts downloading in parallel with the HTML parse rather than waiting for the parser to discover the img tag. Add a preload link in the head:

add_action( 'wp_head', 'mxt_preload_lcp_image', 1 );

function mxt_preload_lcp_image() {
    if ( ! is_singular() || ! has_post_thumbnail() ) {
        return;
    }

    $thumb_id = get_post_thumbnail_id();
    $hero     = wp_get_attachment_image_src( $thumb_id, 'large' );
    if ( ! $hero ) {
        return;
    }

    $srcset = wp_get_attachment_image_srcset( $thumb_id, 'large' );
    $sizes  = wp_get_attachment_image_sizes(  $thumb_id, 'large' );

    printf(
        '<link rel="preload" as="image" href="%s" imagesrcset="%s" imagesizes="%s" fetchpriority="high">',
        esc_url( $hero[0] ),
        esc_attr( $srcset ),
        esc_attr( $sizes )
    );
}

This combination — eager loading + fetchpriority high + preload — is what wins LCP on real sites, not just lab tests.

What to lazy-load and what not to

  • Lazy: below-the-fold inline images, image galleries that scroll out of view, comment avatars, related- post thumbnails, footer images
  • Eager + preload:hero/LCP images, featured images on single-post pages, logo if it's critical to first paint
  • Don't lazy-load at all: images smaller than ~10KB (the bytes saved are less than the JS overhead of the lazy mechanism), images that are likely to be in the initial viewport on mobile (which is almost everything in the first screen on a 400px-wide device)

Lazy loading iframes

The same loading="lazy" attribute works on iframes. Highly worth it for embedded YouTube/Vimeo videos — those iframes load megabytes of player code even when the user never plays the video.

<iframe src="https://www.youtube.com/embed/abc123"
        loading="lazy"
        title="Demo video">
</iframe>

For even better results, use a thumbnail-and-click pattern (the "poster" image only loads the iframe when clicked) — saves substantial weight from pages with embedded video.

Background images

CSS background-imagedoesn't supportloading="lazy". If you have heavy background images that aren't in the initial viewport, you have three options:

  • Convert to an img element with object-fit: cover in CSS — usually the right move
  • Use IntersectionObserver to assign the background-image URL only when it enters the viewport — works but requires JS
  • Skip background-image entirely for non-essential decorative images — often the right call

Common mistakes

  • Adding a JS lazy-load library on top of native lazy-loading. Conflict; pick one. In 2026, native wins.
  • Lazy-loading the LCP image. The single most damaging misconfiguration.
  • Skipping width/height attributes.Causes CLS; browsers can't reserve space.
  • Lazy-loading every img on the page indiscriminately. Hurts above-the-fold images. Use eager for the hero region specifically.
  • Treating lazy loading as a substitute for image optimization.Compress your images. Lazy loading defers the cost; it doesn't eliminate it.

Lazy loading is a small set of careful decisions, not a plugin you install and forget. The above patterns get the eager loading of the LCP image right, defer everything else, and keep the JavaScript-free native implementation as the primary mechanism. For the broader WordPress performance discipline this fits inside, see WordPress Performance Optimization Checklist. For the metrics this is moving and how Google measures them, see Core Web Vitals 2026: The Technical SEO Blueprint.

Need help putting this into practice?

MaxtDesign builds the AI-powered web stacks the articles describe — from agentic workflows to performance-first WordPress + WooCommerce. Talk to us about your project.