Heckgalerie

Lazy Loading in WordPress: What the Browser Already Does, and Where a Plugin Hurts

For years now, browsers have fetched images and iframes only when they get close to the screen, and WordPress writes the attribute for you. An extra lazy-load plugin mostly duplicates what is already there. In one place it does real damage: the largest image in the first screen.

For a long time, lazy loading was a reason to install a plugin: a script swapped placeholders for real images as visitors scrolled. Today the browser does that with a single HTML attribute, and WordPress writes it into your markup unasked.

If you still carry a lazy loader from the old days, you have two mechanisms doing one job. At best that is redundant. At worst it holds back the one image your visitors wait for longest.

What loading="lazy" actually does

The attribute asks the browser not to fetch an image straight away. According to the MDN reference for the img element, the browser defers loading until the image reaches a distance from the viewport that the browser calculates itself. You do not set that distance. The browser does, and it knows the connection and the screen.

Embedded pages work the same way, because the iframe element supports loading="lazy" too and is then fetched only once it nears the visible area. That covers embedded videos, maps and third-party forms.

<img src="harbour.jpg" width="1200" height="800" loading="lazy" alt="Container ship at the quay">

Mind the width and height. Without them the browser cannot reserve space before the image arrives, so the text below jumps, and with lazy images that jump happens mid-read. WordPress draws a firm conclusion from this.

What WordPress has done automatically for years

You do not need to add the attribute yourself. Core has handled it since 2020 and tightened the rules several times. Every row below is based on the developer note on make.wordpress.org.

VersionWhat core doesSource
5.5loading="lazy" on every img that has a width and height: in post content, excerpts, text widgets, avatars and images output through wp_get_attachment_image(). Missing dimensions are filled in for media library images.Note of 14 July 2020
5.7The same for iframes, including automatically embedded videos, but only when the iframe comes with a width and height.Note of 19 February 2021
5.9The first image or iframe in the content is left without the attribute, because it almost always sits in the first screen. A filter changes that number.Note of 29 December 2021
6.3The first three are skipped instead of one. The image WordPress considers the LCP image gets fetchpriority="high", which according to the note typically improves LCP by 5 to 10 percent.Note of 13 July 2023

So a current WordPress with a properly built theme lazy-loads images further down and prioritises the main image, without you touching a setting.

It is still a guess. WordPress decides on the server which image is probably at the top, without knowing the visitor's screen or what your theme's CSS does. On a single-column blog that usually works. On home pages with tile grids, sliders or a page builder, check it.

Why the LCP image must never be lazy

LCP stands for Largest Contentful Paint: the moment the largest element in the first screen becomes visible. Usually a featured image, hero photo or video preview, sometimes a block of text. Google counts it among the Core Web Vitals, and a good LCP is 2.5 seconds or less.

The browser only requests an image with loading="lazy" once it has worked out the layout and knows where the image sits. Far down the page, that is correct. For the most important image, it is the wrong order: it waits for a step it does not need. Google's guide to optimising LCP says so without any hedging: never lazy-load your LCP image, because it always causes an unnecessary delay.

WordPress learned how real this is in its own core. In the first version from 2020, nearly every image received the attribute, including the top one. Felix Arntz and Rick Viscomi analysed the effects for web.dev (last updated March 2022): 84 percent of sites using browser-level lazy loading ran on WordPress, and in a lab test with the default Twenty Twenty-One theme, LCP on archive pages improved by 13 to 15 percent once lazy loading was switched off, while single post pages showed hardly any difference. The fix arrived in version 5.9, see the table.

Why an extra lazy load plugin rarely helps

Classic lazy-load scripts follow a pattern you can spot in the source. The real image address moves from src into a stand-in attribute such as data-src, a placeholder takes its place, and a script puts the address back once the image comes into view. Before the attribute became a web standard in 2020, there was no other way. Today it has four drawbacks:

  • The browser discovers the image too late. Normally it starts downloading images as soon as it reads the HTML. Given a placeholder, the image waits for the script, and the script waits for everything before it.
  • The top image is hit hardest. A script that treats every image the same also delays the LCP image. WordPress fixed that very mistake in its own core with 5.9 and 6.3.
  • Two mechanisms for one job. When something does not load, you are searching in two places.
  • No JavaScript, no image. If the script fails, say because of an error in an unrelated plugin, the placeholder stays unless there is a noscript fallback.

Our theme switch case study shows how expensive a script-loaded main image can get. After moving to a fast WordPress theme, mobile LCP still sat at 4 to 6 seconds, because the largest element on the key pages was a video poster that was only fetched later via JavaScript. In a second round, that preview image was preloaded with high priority and shown immediately as a CSS background instead of waiting for the embed script.

On the course page, LCP then dropped from 5.2 to 4.0 seconds, measured on mobile with Lighthouse. Two caveats belong here. The same round included other measures, so we did not isolate the poster's share. And 4.0 seconds is still above the 2.5 second mark.

Our recommendation for a current WordPress: switch off lazy-load features in plugins, leave the job to core, measure again. If you keep a plugin's lazy load, exclude at least the main image.

How to check whether your LCP image is accidentally lazy

The quick look: Inspect

  1. Open the page in Chrome, press F12 and switch on device mode with Ctrl+Shift+M, since the largest element on a phone can differ from desktop.
  2. Right-click the large image in the first screen, then choose Inspect.
  3. Look at the img tag. loading="lazy", or data-src instead of src plus a class like lazyload, means your most important image loads late.
  4. If it has fetchpriority="high", WordPress or your theme recognised it correctly.

The thorough look: Performance panel and Lighthouse

The Performance panel in DevTools and Lighthouse both include the "LCP request discovery" insight. According to the Chrome documentation, it checks whether the LCP image is discoverable straight from the HTML, whether it carries fetchpriority="high" and whether it avoids loading="lazy".

For the curious: one line in the console

new PerformanceObserver(l => { const e = l.getEntries().at(-1); console.log(e.element, e.element?.loading, Math.round(e.startTime) + ' ms'); }).observe({ type: 'largest-contentful-paint', buffered: true });

Paste it into the DevTools console and press Enter. It prints the LCP element, its loading value and the time in milliseconds. If it says lazy, you have found the problem. The timing reflects your machine and connection, not a controlled measurement.

If you find something

  • Lazy loader in a plugin: add an exception for the main image or switch the feature off.
  • Image in a theme template: if it is output with wp_get_attachment_image(), pass 'loading' => false and WordPress leaves the attribute out.
  • Hero as a CSS background: the browser only sees it after reading the stylesheet. A high-priority preload in the page head helps here, as in our case study:
<link rel="preload" as="image" href="/images/hero.webp" fetchpriority="high">

When a plugin or a few lines of script do make sense

Background images set in CSS

The attribute exists for img and iframe. Asked whether CSS background images can use it, Google's guide to browser-level lazy loading answers with a plain no. So for large background images far down the page, the kind page builders like to put into sections, there is no built-in brake.

Here a script earns its place: one that adds the class carrying the background image only when the section comes into view, via IntersectionObserver. Often a rebuild is better. Where a background image is really content, it belongs in the HTML as an img, and the browser takes over again.

Embedded videos: a facade instead of the player

For YouTube videos, WordPress has added loading="lazy" to the iframe since 5.7, provided the dimensions are there. That postpones loading without preventing it: once the video nears the screen, the browser loads the full player and connects to YouTube, even if nobody presses play.

That is why hafenstudios.com embeds videos as a facade. The HTML contains no iframe, just a link to the video on YouTube with a preview image served from our own server. Only a click replaces the link with an iframe pointing to youtube-nocookie.com, which starts playing right away. Until then the facade loads nothing from YouTube, and without JavaScript it stays an ordinary link to YouTube. Simplified and without styling, it looks like the one in the post with our first video:

<a class="yt-facade" href="https://youtu.be/VIDEO-ID" data-ytid="VIDEO-ID">
  <img src="/assets/blog/preview.jpg" width="1280" height="720" decoding="async" alt="…">
</a>
<script>
document.querySelector('.yt-facade').addEventListener('click', function (e) {
  e.preventDefault();
  var i = document.createElement('iframe');
  i.src = 'https://www.youtube-nocookie.com/embed/' + this.dataset.ytid + '?autoplay=1';
  i.allow = 'autoplay; encrypted-media; picture-in-picture';
  i.allowFullscreen = true;
  this.innerHTML = '';
  this.appendChild(i);
});
</script>

The preview is an ordinary img with the same rules as any other. In our video posts, the facade sits near the top of the text and has no loading attribute. On the product page of our ad plugin Adjet it only appears in the third section. In our lab audit of 28 September 2026, the German version of that page had a mobile LCP of 2.5 to 2.6 seconds, the only one of 14 measured pages above 2.5 seconds, and the audit noted a 129 KB preview image loading immediately. Since 29 September it carries loading="lazy" in every language version and is served as WebP. The audit has no follow-up measurement, so we quote no after figure.

Galleries with many images

Galleries are where lazy loading saves the most. A page with 60 photos loads only those near the screen, the rest follow on scroll. No gallery plugin needs its own lazy loader for that, but three things should be in order:

  • Width and height on every image. Without both, WordPress does not add the attribute at all, and the grid jumps as images arrive.
  • srcset and sizes. Lazy loading saves the images nobody sees. For the ones that do load, the right size decides: a phone does not need a full-width file for a narrow tile. Media library images get srcset from WordPress.
  • Do not delay the first row. If the gallery sits at the very top, its first images are LCP candidates. Since 6.3 WordPress skips the first three content images. If your first row shows four or five, raise the threshold for that page:
add_filter( 'wp_omit_loading_attr_threshold', function ( $threshold ) {
	return is_page( 'gallery' ) ? 8 : $threshold;
} );

The filter dates from WordPress 5.9 and belongs in a child theme's functions.php or a snippets plugin. Set the number to the images in your first screen. No more, or you lose the savings further down.

If your images live outside the media library, say in a gallery plugin's own tables and folders, core adds neither srcset nor missing dimensions, because it can only work those out for media library attachments. In our gallery plugin Heckgalerie, a gallery is a list of ordinary media library attachments: srcset and image sizes come from WordPress, grids and logo bars work without JavaScript, and scripts load only on pages that contain a slider or a lightbox (as described in its WordPress.org listing, as of 4 October 2026). If you are moving away from a heavy gallery plugin, our comparison of NextGEN Gallery alternatives lists the candidates.

Work through it in this order

  1. Keep WordPress up to date, from 6.3 on core does the groundwork.
  2. Switch off duplicate lazy loaders. One is enough, and core already is one.
  3. Check the LCP element on mobile and desktop: no loading="lazy", no data-src.
  4. Handle background images and videos on purpose.
  5. Measure before and after, with the same method. Whatever else costs speed is in our guide on how to speed up a slow WordPress site.
Rather have it measured? Our speed optimisation service gives you a report and the implementation, with before and after figures, at a fixed price from 890 €.

Galleries from your media library

Heckgalerie shows galleries, logo bars, sliders and albums built from ordinary media library images. Grids and logo bars need no JavaScript, and the plugin loads scripts only where a slider or lightbox sits.

Heckgalerie on WordPress.org

Frequently asked questions

Do I need a lazy load plugin for WordPress?

On a current WordPress, usually not. Core has added loading="lazy" to images since version 5.5 and to iframes since 5.7, and since 6.3 it skips the first three content images. A plugin that also swaps images in with JavaScript duplicates that work and can delay your most important image. Extra help is worth it for CSS background images and embedded videos.

How can I tell if my LCP image is lazy loaded?

Right-click the large image in the first screen and choose Inspect: if the img tag has loading="lazy", or data-src instead of src, it loads late. For a thorough check, use the "LCP request discovery" insight in the Chrome Performance panel or in Lighthouse. Check the mobile view too, since a different element can be the largest there.

How do I turn off lazy loading in WordPress?

One line in a child theme's functions.php or a snippets plugin switches it off entirely: add_filter( 'wp_lazy_loading_enabled', '__return_false' ); That is rarely wise, because lazy images further down save data. Exclude only the main image instead, with 'loading' => false when it is output through wp_get_attachment_image().

Should I lazy load YouTube videos in WordPress?

Yes, ideally with a facade rather than the attribute alone. WordPress has added loading="lazy" to embedded videos with a width and height since version 5.7, but near the screen the browser still loads the full player. A facade shows only a preview image from your own server and builds the iframe on click. The page then connects to YouTube only when someone wants to watch.

What does fetchpriority="high" do on images?

It tells the browser that this image matters more than the others, even before layout has been calculated. WordPress has added it automatically since version 6.3 to the likely LCP image, and the developer note puts the typical gain at 5 to 10 percent. Use it on one image, two at most, and never together with loading="lazy" on the same image.

Is lazy loading worth it for galleries with many images?

Yes, that is where it saves the most, and the browser's own attribute is enough. Every image needs a width and height, plus srcset so phones get small files. If the gallery sits at the very top, its first row should not load late: since 6.3 WordPress skips three content images, and the wp_omit_loading_attr_threshold filter raises that number.

Back to blog A post by hafenstudios