RevealTheme logo
Back to Blog

Why Most Sites Configure Lazy Loading Wrong

Why Most Sites Configure Lazy Loading Wrong
The RevealTheme Team

By

·

Lazy loading is one of the few performance wins WordPress gives you essentially for free. Since version 5.5, core has added loading="lazy" to images automatically, and the result is usually faster pages and lighter bandwidth bills. Yet a surprising number of sites end up slower after enabling it, and almost always for the same reason: the optimization gets pointed at the wrong images, or stacked on top of itself by three plugins that don't know the others exist.

The mechanism is simple, which is exactly why it's easy to break. loading="lazy" tells the browser to defer an image's network request until it's about to scroll into view. That's pure upside for an image 4,000 pixels down the page that a visitor may never reach. It's actively harmful for the first image they see.

The one image that must never be lazy

Your Largest Contentful Paint element is the single biggest piece of content rendered in the initial viewport. On most WordPress layouts that's a hero image, a featured image at the top of a post, or a full-width banner. Google treats LCP as a Core Web Vitals metric, and the threshold to pass is LCP under 2.5 seconds for 75% of page views.

When you lazy-load the LCP image, you instruct the browser to delay the request that should have been its highest priority. The browser's preload scanner would normally spot that image in the raw HTML and start fetching it immediately, before CSS or JavaScript even finishes parsing. Add loading="lazy" and that head start evaporates. The image now waits for layout to settle so the browser can decide whether it's near the viewport — and by then you've lost several hundred milliseconds on the metric that matters most.

This is the signature failure: someone enables a lazy-loading plugin expecting a faster site, watches their PageSpeed Insights score drop, and concludes lazy loading "doesn't work." It works fine. It was simply applied to the one image that should have been eager.

Why core usually gets this right and plugins often don't

WordPress core is smarter than people give it credit for. Since 5.9 it skips lazy loading on the first image (and on images that appear before the main content), specifically to protect the likely LCP candidate. If you're running nothing but core's native behavior on a typical theme, you're probably already in good shape and don't need to touch anything.

The trouble starts when a third-party optimizer layers its own logic on top. Plugins like WP Rocket, LiteSpeed Cache, Perfmatters, Autoptimize, and a3 Lazy Load each ship their own lazy-loading engine, and several of them historically applied loading="lazy" — or a JavaScript-based equivalent that swaps data-src for src — to every image indiscriminately, including the hero. The JavaScript variants are worse than native lazy loading here, because the image can't even begin loading until a script runs, runs its viewport math, and rewrites the DOM.

The fix is configuration, not removal. Every one of these plugins now offers an exclusion list. In WP Rocket it's "Excluded images or iframes." In Perfmatters it's an "Exclude from Lazy Loading" field. LiteSpeed has a "Lazy Load Image Excludes" box. You add the LCP image's URL, filename fragment, or CSS class, and the plugin leaves it alone.

Finding the exact element to exclude

Don't guess. Run the page through PageSpeed Insights, expand the diagnostics, and look for the line "Largest Contentful Paint element." It names the precise node. Open the page source, find that <img>, and check whether it carries loading="lazy" or a data-src placeholder. If it does, that's your culprit — add it to the exclusion list and re-test. Recovering the LCP element from lazy loading commonly shaves a meaningful chunk off the metric on hero-driven layouts.

The stacking problem nobody warns you about

Here's the trap I see constantly on real sites: lazy loading is enabled in more than one place at once. Core does it. The caching plugin does it. The page builder (Elementor and Divi both have their own image lazy-load toggles) does it too. Now you have three systems fighting over the same <img> tags, and the most aggressive one wins — usually the one that rewrites src to data-src, which defeats core's careful first-image exclusion entirely.

Pick one lazy-loading source and disable the rest. If you run a performance plugin, let it own lazy loading and consider disabling core's via add_filter('wp_lazy_loading_enabled', '__return_false') only if the plugin truly replaces it. If you run a bare theme, let core handle it and turn the feature off in your page builder. One engine, one exclusion list, predictable behavior.

The contradiction: preloading and lazy loading the same image

This one is genuinely common and quietly wasteful. Many "optimize LCP" guides tell you to add a <link rel="preload"> for your hero image. Many performance plugins offer a "Preload LCP image" toggle that does exactly that. Great — except the same plugin may also be lazy-loading that image. You're now telling the browser "fetch this immediately, top priority" and "don't fetch this until it's visible" simultaneously.

The browser resolves the conflict in favor of laziness, the preload hint is ignored, and you've burned a request slot reserving bandwidth that never gets used. If you preload the hero, you must exclude it from lazy loading. The two settings are only correct as a matched pair.

The cases where loading="lazy" simply doesn't apply

A few categories silently fall outside the attribute's reach, and people lose hours not realizing it:

  • CSS background images. loading="lazy" only works on real <img> and <iframe> elements. Anything set with background-image: url(...) in CSS has no native lazy equivalent. If a heavy section background is dragging down load time, you need JavaScript-based deferral or a redesign that uses an <img>.
  • Autoplaying carousels and sliders. Lazy-loading slides that rotate automatically produces a visible blank flash on each transition, because the next slide's request doesn't fire until it's already needed. Exclude carousel images, or preload the next slide.
  • Above-the-fold logos and icons. Tiny images cost almost nothing to load eagerly, and lazy-loading them adds JavaScript overhead and layout jank for no bandwidth saving worth measuring.

A pragmatic configuration that actually works

If you want a setup that holds up across themes and plugins, this is the shape of it:

  1. Decide who owns lazy loading — core or one performance plugin, never both plus a page builder.
  2. Identify your LCP element with PageSpeed Insights on your most-trafficked template (home, post, product).
  3. Exclude that element from lazy loading, and exclude any autoplay carousel images.
  4. If you preload the hero, confirm it's also excluded from lazy loading — check for the contradiction.
  5. Add explicit width and height (or aspect-ratio) to every lazy image so deferred loading doesn't trigger layout shift and wreck your CLS score.
  6. Re-measure with field data, not just lab scores. Lighthouse runs a single simulated load; real users on slower connections are what Core Web Vitals actually grades.

Configured this way, lazy loading does what it's supposed to: a text-heavy post with a couple of images sees a modest improvement, while an image-dense gallery or long-scroll article can shed a serious amount of unused initial download. The benefit scales with how much below-fold imagery you have. The damage, on the other hand, comes almost entirely from a single misplaced attribute on a single important image — which means the fix is just as targeted as the mistake.