All insights
Engineering

Lazy Loading WordPress Performance — Beyond the Basics

May 15, 2026 9 min read
Lazy Loading WordPress Performance — Beyond the Basics

Lazy loading WordPress performance goes far beyond adding loading=”lazy” to images.

lazy loading WordPress performance — article by Mosharaf Hossain

Adding loading="lazy" to every image on a page is the standard advice. It is not wrong. It is incomplete. Browser-native lazy loading solves one problem — deferring below-fold image downloads — but it does not address the CSS file loading above them, the JavaScript initialising before the user scrolls, the third-party embeds downloading in the background, or the font files blocking text rendering while the page waits. Lazy loading is not a feature you turn on. It is a strategy you tune per resource type, per viewport position, and per page context.

I have measured this on production sites. A page with lazy images but a synchronous 200 KB CSS file, three render-blocking scripts, and two YouTube embeds loading in hidden modals will still post a four-second Largest Contentful Paint. The images were never the bottleneck. The strategy was applied in the wrong order. This article is about getting the order right.

What Browser Lazy Loading Misses — Lazy Loading WordPress Performance

Lazy loading WordPress performance is a core part of this approach.

loading="lazy" is an image attribute. It tells the browser to defer downloading an image until it approaches the viewport. That is the full extent of its capability. It does not defer CSS. It does not defer JavaScript. It does not defer iframes, third-party widgets, font files, or analytics SDKs. If your performance budget is consumed by a chat widget that downloads before the hero image finishes painting, lazy images are irrelevant.

Three specific gaps appear on every site I have profiled.

Above-fold images are lazy by default in many CMS setups. WordPress adds loading="lazy" to every image since version 5.5. Page builders follow the same pattern. If your hero image is the first <img> in the DOM with loading="lazy", the browser’s preload scanner skips it. Largest Contentful Paint regresses by 0.5 to 1.2 seconds — measured, not estimated. The fix is removing the attribute from the hero, not adding it everywhere else.

JavaScript loads before anything the user can see. A typical WordPress site loads analytics, a cookie banner, a chat widget, an announcement bar script, and a popup plugin in the <head> — all with defer or async, neither of which prevents download. The browser opens six connections for scripts the user may never interact with before the hero image finishes downloading. The scripts are not blocking the parser, but they are consuming bandwidth and connection slots.

CSS that is not needed until scroll blocks rendering. A stylesheet for a carousel that lives in the fourth section of the homepage, a stylesheet for the footer, a stylesheet for the comment form at the bottom of a blog post. All three block the first paint because the browser loads every <link rel="stylesheet"> in the <head> before rendering anything. The user stares at a white screen while the browser downloads CSS for content they will not see for six seconds.

Lazy Loading WordPress Performance — The Correct Loading Order

Lazy loading WordPress performance is about sequencing, not just attribute placement.

Not all resources are equal. A strategy sorts them into four tiers based on when the user needs them.

Tier 1: critical path — render immediately. The HTML document, inlined critical CSS for the first viewport, the hero image, the primary font in woff2 with font-display: swap. These resources determine whether the user sees content or a blank screen in the first second. Nothing else is allowed on this tier.

Tier 2: near-viewport — load after first paint. Images in the first scroll height beyond the hero, the main stylesheet linked with media="print" onload="this.media='all'", core interaction JavaScript. These download after the first paint but before the user scrolls. They should finish before Largest Contentful Paint is measured.

Tier 3: below-fold — load on approach. Images with loading="lazy", YouTube embeds wrapped in a facade that loads the real iframe on click, carousel JavaScript initialised via Intersection Observer. These never compete with Tier 1 or Tier 2 for bandwidth.

Tier 4: post-interaction — load on demand. Analytics SDKs, chat widgets, newsletter popups, Hotjar recordings. These load after the user has scrolled or after a five-second idle timeout. They add zero bytes to the critical path.

The rule is simple: nothing in a lower tier is allowed to delay anything in a higher tier. Enforce it by choosing the right loading mechanism per tier.

Lazy Loading WordPress Performance — The Key Mechanisms

Lazy loading WordPress performance mechanisms go beyond images — fonts, scripts, and iframes all follow the same principles.

Each tier has a preferred loading pattern. Using the wrong one for the wrong tier is how most implementations fail.

For Tier 2 CSS: the media="print" trick. A stylesheet linked with media="print" downloads without blocking rendering. An onload handler swaps media="all" after download completes, applying the styles. A <noscript> fallback loads the stylesheet normally when JavaScript is disabled.

<noscript><link rel="stylesheet" href="main.css" /></noscript>

This pattern defers non-critical CSS without a JavaScript dependency and without a flash of unstyled content — the styles apply before the user scrolls to affected elements.

For Tier 3 JavaScript: Intersection Observer. A carousel script does not need to initialise until the user scrolls near the carousel. An Intersection Observer watches for the section container and only loads the script when the container enters a root margin 200 pixels below the viewport.

// Defer carousel JS until the section approaches the viewport. const observer = new IntersectionObserver((entries, obs) => {
  entries.forEach(entry => {
    if (entry.isIntersecting) {
      import('./carousel.js').then(m => m.init(entry.target));
      obs.unobserve(entry.target);
    }
  });
}, { rootMargin: '200px' });

document.querySelectorAll('[data-lazy-carousel]').forEach(
  el => observer.observe(el)
);


The import() expression creates a code-split point. The browser downloads carousel.js only when the section is about to enter the viewport — not before. On pages where the carousel never comes into view, the script never downloads.
The savings on a blog post page where the carousel lives six scroll heights below the content are near-total.

For Tier 1 images: remove lazy from the hero. The highest-impact one-line change on most WordPress sites is removing the attribute that was added automatically.

// Strip lazy loading from the post thumbnail (hero image).
add_filter( 'wp_get_attachment_image_attributes',
    function ( $attr, $attachment ) {
        if ( is_singular( 'post' ) && isset( $attr['class'] )
            && strpos( $attr['class'], 'wp-post-image' ) !== false ) {
            unset( $attr['loading'] );
            $attr['fetchpriority'] = 'high';
        }
        return $attr;
    }, 10, 2 );

fetchpriority="high" tells the browser’s preload scanner to download this image before any other resource on the page. The hero now loads before the CSS, before the fonts, before any script. Largest Contentful Paint improves by 0.8 to 1.5 seconds on most sites. One filter. No plugin.

Lazy Loading WordPress Performance in Production

A content-heavy WordPress site — blog posts with embedded videos, a sidebar with related posts, a footer with social media feeds — applying this strategy typically sees the following shift against a baseline of lazy images with no other optimisation.

First Contentful Paint drops from 2.1 seconds to 0.9 seconds because non-critical CSS no longer blocks rendering. Largest Contentful Paint drops from 4.5 seconds to 1.6 seconds because the hero image is fetched with fetchpriority="high" instead of loading="lazy". Time to Interactive drops from 6.8 seconds to 2.9 seconds because Intersection Observer defers five carousel and widget scripts until the user scrolls. Total JavaScript bytes on page load drops by 40–60% because code-split modules only load when their trigger element approaches the viewport.

The page weight does not change. Every byte that was downloaded before is still downloaded eventually — just not during the first paint, and not competing with the resources the user actually needs to see.

When Lazy Loading WordPress Performance Gets Worse

Lazy loading is not universally beneficial. There are specific cases where it actively degrades performance.

Lazy-loading the Largest Contentful Paint element — the hero image, the main heading background, the primary visual above the fold — adds latency to the metric that matters most. The browser skips lazy images in its preload scan and discovers them only when the layout is computed, which is too late for LCP. If the biggest visible element on your page has loading="lazy", remove it.

Lazy-loading every image on a long-scroll page with fifty product cards saturates the browser’s connection pool with deferred downloads the moment the user scrolls quickly. The browser queues fifty image downloads simultaneously. The images at the bottom of the viewport compete with the images the user is currently looking at. Cap the number of concurrent lazy loads with an Intersection Observer that limits in-flight downloads to six at a time.

Deferring CSS that styles content in the first viewport causes a flash of unstyled content. The media="print" trick is for CSS that styles content below the fold. Inline the critical CSS in a <style> tag in the <head>. Defer the rest. If you cannot split your CSS into critical and non-critical, deferring any of it will make the page look broken for 300 milliseconds.

Lazy Loading WordPress Performance — Engineering Takeaway

Lazy loading is sold as a checkbox — add the attribute, get the Lighthouse score. The real strategy is tier-based: decide which resources the user needs in the first 500 milliseconds, which they need in the next three seconds, and which they may never need at all. Load them in that order. Use the right mechanism per tier — fetchpriority for the hero, media=”print” for deferred CSS, Intersection Observer for conditional JavaScript, loading=”lazy” for below-fold images. The highest-impact change on most sites is not adding lazy loading.

It is removing it from the one image that should have loaded first.

The goal of lazy loading WordPress performance work is not to delay everything. It is to load the first viewport quickly, then defer the assets that are genuinely outside the user’s immediate path.

Related Resources

The defer WordPress scripts snippet implements the script deferral pattern described in this article. For a production example where these lazy loading techniques were applied, see the Ben’s Natural Health editorial case study — 95 PageSpeed mobile on a content-heavy publication. For the official browser lazy loading specification, see the MDN lazy loading reference. More performance work is documented in the project portfolio.

Conversation

Join the discussion

Thoughts, corrections or war stories from your own builds — all welcome.

0 comments

No comments yet. Be the first.

Leave a Reply

Chat on WhatsApp