Lazy load images in WordPress without a plugin

Images are consistently the largest contributors to page weight on WordPress sites. A typical blog post with six to eight inline images easily transfers one to three megabytes of image data to each visitor, regardless of whether those images are visible on screen when the page loads. A long recipe post, a product review with comparison shots, or a photo gallery page might render the first screen of content from just one hero image and a block of text, while the browser simultaneously downloads every other image further down the page that the user has not yet scrolled to and may never see. This wasted bandwidth slows down initial page rendering, inflates mobile data usage for visitors on cellular connections, and increases time to first contentful paint — a core metric in Google’s Lighthouse scoring and a ranking factor in their search algorithm. Lazy loading addresses this by deferring the loading of off-screen images until the user scrolls close enough that they are about to enter the viewport. From WordPress 5.5 onwards, core natively adds the loading="lazy" HTML attribute to all images inserted via the block editor and the wp_get_attachment_image() function. This native browser-level lazy loading requires zero JavaScript, works in all modern browsers, and delivers real performance gains with no configuration. For images that WordPress does not control — hard-coded <img> tags in theme templates, images output by page builders, or background images set via inline styles — you need to handle lazy loading separately. A PHP filter can retrofit the loading="lazy" attribute onto all images passing through the_content filter, covering the majority of post body images. Images in widgets, theme headers, and custom template parts require manual attribute addition or a JavaScript-based observer. Compared to plugin-based solutions that often bundle their own JavaScript intersection observer and custom markup, the native attribute approach is lighter, faster to initialize, and has no dependency on JavaScript being enabled or a third-party script loading correctly. For most WordPress blogs and business sites, enabling native lazy loading for content images requires nothing more than the filter below.

Problem: Images below the fold load immediately on page load, wasting bandwidth and slowing initial page rendering.

Solution: Add the following code to your functions.php file to retrofit loading="lazy" onto all content images:

<?php
// WordPress 5.5+ adds loading="lazy" to attachment images automatically.
// This filter covers any remaining <img> tags in post content.
add_filter( 'the_content', 'ha_add_lazy_loading_to_content_images' );

function ha_add_lazy_loading_to_content_images( $content ) {
    // Skip if the tag already has a loading attribute
    if ( ! str_contains( $content, '<img' ) ) {
        return $content;
    }
    // Add loading="lazy" to img tags that don't already have it
    $content = preg_replace(
        '/(<img(?![^>]*loading=)[^>]*)(>)/i',
        '$1 loading="lazy"$2',
        $content
    );
    return $content;
}

NOTE: WordPress 5.5 and above already handles lazy loading for images inserted through the block editor and wp_get_attachment_image(), so this filter primarily benefits sites still using older themes or classic editor markup. Do not apply lazy loading to the first above-the-fold image (typically your featured image or hero banner) — lazy loading the largest contentful paint element delays the LCP metric, which hurts both user experience and Google ranking. Add loading="eager" explicitly to critical above-the-fold images to override the filter. Also see our post on speeding up WordPress admin with functions.php tweaks for more performance wins.