Implement Lazy-Loading without Plugin

Update 11 Aug 2020: WordPress 5.5 introduced native lazy loading by adding loading="lazy" attribute to all images. So you no longer need to create your own.

Lazy-load is a way to prevent images from loading before we scroll past it. There are many plugins that help you with this, some themes even have a built-in lazy-load function.

Here we will show you that it’s not hard to implement it yourself.

Step 1: Replace SRC Attribute

<!-- Convert this -->

<img src=".." srcset="...">

<!-- into -->

<img data-src="..." data-srcset="...">

When an image has src or srcset attribute, it will always load no matter what. So our first step is to temporarily prefix them with data-.

In WordPress, we can filter the content and replace them with regex:

add_filter( 'the_content', 'my_lazyload_content_images' );
add_filter( 'widget_text', 'my_lazyload_content_images' );
add_filter( 'wp_get_attachment_image_attributes', 'my_lazyload_attachments', 10, 2 );

// Replace the image attributes in Post/Page Content
function my_lazyload_content_images( $content ) {
  $content = preg_replace( '/(<img.+)(src)/Ui', '$1data-$2', $content );
  $content = preg_replace( '/(<img.+)(srcset)/Ui', '$1data-$2', $content );
  return $content;
}

// Replace the image attributes in Post Listing, Related Posts, etc.
function my_lazyload_attachments( $atts, $attachment ) {
  $atts['data-src'] = $atts['src'];
  unset( $atts['src'] );
  
  if( isset( $atts['srcset'] ) ) {
    $atts['data-srcset'] = $atts['srcset'];
    unset( $atts['srcset'] );
  }

  return $atts;
}

Step 2: Add Scroll Listener

When the image reaches your viewport, it should replace data-src into src, that way the image will start loading.

( function() { 'use strict';
  let images = document.querySelectorAll('img[data-src]');
              
  document.addEventListener('DOMContentLoaded', onReady);
  function onReady() {
    // Show above-the-fold images first
    showImagesOnView();

    // scroll listener
    window.addEventListener( 'scroll', showImagesOnView, false );
  }
  
  // Show the image if reached on viewport
  function showImagesOnView( e ) {
    
    for( let i of images ) {
      if( i.getAttribute('src') ) { continue; } // SKIP if already displayed
      
      // Compare the position of image and scroll
      let bounding = i.getBoundingClientRect();
      let isOnView = bounding.top >= 0 &&
      bounding.left >= 0 &&
      bounding.bottom <= (window.innerHeight || document.documentElement.clientHeight) &&
      bounding.right <= (window.innerWidth || document.documentElement.clientWidth);
      
      if( isOnView ) {
        i.setAttribute( 'src', i.dataset.src );
        if( i.getAttribute('data-srcset') ) {
          i.setAttribute( 'srcset', i.dataset.srcset );
        }
      }
    }
  }
              
})();

Don’t forget to enqueue the script:

add_action( 'wp_enqueue_scripts', 'my_lazyload_assets', 10 );
function my_lazyload_assets() {
  $js_dir = get_stylesheet_directory_uri() . '/js';
  wp_enqueue_script( 'my-lazyload', $js_dir . '/lazyload.js', [], '', true );
}

Step 3: Add Animation

You can customize the animation purely with CSS. For example, here’s a simple fade-in effect:

img[data-src] {
  opacity: 0;
  transition: opacity .25s ease-in-out;
  will-change: opacity;
}

/* appear animation */
img[data-src][src] {
  opacity: 1;
}

Feel free to change the transition timing to your taste and play around with translate.


Conclusion

Lazyload has become a necessity in all blogs and corporate websites. You don’t have to worry about SEO because Google themselves has confirmed that they can detect lazy-load.

There’s nothing wrong with using a plugin for this feature, but it’s good to know how it’s done. You can also customize it to your needs like customizing the animation and adding a delay.

If this code doesn’t work for you, please let us know in the comment below. It might clash with your themes or some plugins.

Default image
Henner Setyono
A web developer who mainly build custom theme for WordPress since 2013. Young enough to never had to deal with using Table as layout.

43 Comments

  1. Thank you for share this. I will use in my theme.

  2. Thank you so much for the great article and explanation. There are some pictures that are not using the lazy load method. That is most likely because of the theme but if you can provide some solution, that would be much appreciated.

    • Hi thanks for reading!

      Is the image part of main content or on Header / Footer?

      You need to add some filters depending on your theme. But you should be able to use the same regex to replace the src attribute.

      • Hi Henner, it is mainly for the pictures in the section that added by some additional features in the theme. If you check the page speed insight score, it shows which exact pictures are not getting the lazycode as how it supposed to be.

  3. Thank you for sharing this! I only have one issue with the code. Unfortunately, it also switches src to data-src in all emails, making email images impossible to render. Do you have any fix for that?

    • Hi Anna, You're welcome!

      Ah I was not aware that the code also affect email. What email plugin are you using?

      First, try checking which filter is applied to the email, is it the_content or wp_get_attachment_image_attributes?

      Maybe you can add a conditional check at the top of the function by checking what's inside global $pagenow

  4. Hi dear, Thanks a lot for sharing and great article! Lazyload is working perfect! But I have an issue.. the lazyload is not working if the image is coming from the Gutenberg Editor and it will wrap a "figure tag" to "img tag". Can we fix the issue?

    • Hi @Lyricsofindia,

      You're welcome!

      I just tested it again and it works on Gutenberg.

      Can you try checking whether the filter is applied? Try adding var_dump('anything'); in the function and see whether it's running.

  5. Cornerstone College
    Cornerstone College

    Thank you very much for the amazing tip! Some of our images on the main page are not being affected by the lazy load feature as they don't have data-src and data-srcset values... I believe that if we can add a data-src and data-srcset values for any image, the problem would be fixed. Would it be possible for you to help me to figure this out?

    • Hi Cornerstone College,

      You're welcome!

      Yes, unfortunately not every image will be affected because it depends on how the theme outputs the image.

      May I know which images are not affected? If it's on Header or Featured Image, then that's safe to not lazyload because they're at the top anyway.

      If its background-image then it's going to take a different approach.

  6. Thank you very much for this beautiful and simple article. I used it on my underscored theme and it worked like a charm. One question. What if I don't want to use a js file but add the script into the header instead? Thanks in advance.

    • Hi Sam,

      Glad you find it useful!

      It's better to put it on JS file so the browser can cache it. If you put the code directly under <script> tag, then your visitor will need to load that in every page he opens.

  7. Thank you for the nice and elegant script. Small question: shouldn’t the line
    `if( i.getAttribute(‘srcset’) ) {`
    be actually like this:
    `if( i.getAttribute(‘data-srcset’) ) {`

  8. Thank you Henner, for your blog and its useful content. Is there a way to exclude featured images from the lazy load effect?

    • Hi Alfredo,

      The featured image is usually printed with the filter wp_get_attachment_image_attributes, so you can add conditional on the function my_lazyload_attachments().

      I'm not sure how your theme does it, but you can check the class attribute. If it contains the class for featured image, prematurely return the $atts.

  9. Hi,

    thanks so much for the code, really helpful! However, the code in the functions.php lets thumbnails disappears in the media library in the WordPress admin. When I remove the code from the functions.php they reappear, so it's definitely root caused by it. Is there any workaround available?

    Thanks upfront! Martin

    • Hi Martin,

      I have just checked and my Media Library is fine (both in Media page and Media popup). Probably you have a plugin that alters Media Library to use wp_get_attachment_image_attributes.

      In that case, you can simply wrap that filter with if( !is_admin() ) { ... }.

  10. Hi, very powerfull tool and its works perfectly, thanks! Forshare.

    A quesiton, how i control the height that to user have to scroll to the imagen appears? I would like to add a little bit less distance in the time betwen the imagen appears in the screen and the lazy loads activate to show the photo.
    How i control that distance? Many thanks, i aprreciate you help, thanks

    • Hi Rex, to add a distance, you can tinker the bounding.top conditional. If the top is -50 it means 50px outside the viewport.

  11. Hi, excellent guide and works well in some cases.

    I had a problem when there are images since they already have data-src or srcset, so after I put your codes, in the same image the word data appears twice. Example of how it appears = "data-data-src" Therefore the image is not displayed when this happens.

    Is there anything to do about it? Like if there is already data-src or srcset on the image then your code will not be able to on such images?

    I would love to know if there is a solution to this.

    In any case, in other cases - the code works wonderfully! Thank you so much for sharing!

    • Hi OR, very sorry for late reply, been very busy the past few months. You can tweak the regex into this /(<img.+)(data-)?(src)/Ui, and the replacement into this $1data-$3

  12. Thank you very much for this beautiful and simple article. I used it on my underscored theme and it worked like a charm. One question. What if I don't want to use a js file but add the script into the header instead? Thanks in advance.

    • Hi Shari, thank you for reading. Putting the code inside script tag works fine, but it's not recommended because your browser can't cache it properly.

      If you keep it in JS file, the browser doesn't need to reload it when your visitor moved to new page.

  13. How to add animation to a native WordPress Lazy Load function? Thanks

    • Hi Ivan,

      You can do it with JavaScript, listening to on "load"

      $('.content-wrapper img').on('load', function(){
        $(this).addClass('loaded');
      });

      Then simply use CSS for the animation

      .content-wrapper img {
        opacity: 0;
        transform: translateY(10px);
        transition: all 0.25s ease-in-out;
      }
      .content-wrapper img.loaded {
        opacity: 1;
        transform: none;
      }
  14. Hey, the code works great!

    Only images that are in popups have a problem. I have a store website, and when you click on "Add to cart", then a popup opens with additional products that the user can purchase.

    The pictures of these products are not uploaded and it shows that there is no picture for some reason .. Does anyone know by chance how to improve the code so that it will also work on pictures from Popup?

    • Hi OR, it seems the picture's src inside popup is correctly replaced to data-src. But since our JS only listens to scrolling, it's not shown when popping up.

      So you need to create JS click listener to change the data-src back to src

  15. Hi, thanks so much for this article. I am trying to apply lazy-load to my self-hosted video's on a specific page. I am confused by whether or not WordPress is doing it already or how to make it do this for all video's (I use Elementor Pro but unfortunately there is no lazy load option for self-hosted video's) looking for the HTML or CSS code to add to my video so it will be set to lazy load...

  16. Hi,

    this is a cool article. I think not using a plugin for every case is a good practice 🙂 Thanks.

  17. Hey Henner,

    Currently, I am using the Lazy load Plugin by WProcket for lazy loading images on my blog. Recently Google has been pushing for a better user experience, so I was thinking about reducing the use of plugins on my site, but I am worried whether the WordPress in-built Lazy loading function will be good enough?

    Also, should I avoid lazy loading my website's logo since it's in LCP?

    • Hi Sara,

      Yes, built-in Lazy loading is better now. If you update to latest WP version, any image in your Post's body will have loading="lazy" attribute which is more friendly to Google. If you have Image outside the Post's body, you might need to change the Theme's code to add the attribute

  18. Thank you so much it makes my website load faster. But I have a problem that when I use the slider (specifically Slick) it doesn't work very well. Can you give me a hint on how to handle this.

    • Hi Nguyen,

      As far as I'm aware with Slick, it won't go well with lazy loading. You can try tweaking the Regex to exclude slick images.

  19. Hey, great script so far. I added this to our website, but there is a little issue. On the detail page of our job postings, based WP Job Manager, the images are not showing up. Any idea about this?

    • Hi Bjoern,

      I'm not aware of how WP Job Manager render things. But you should be able to tune the Regex to correctly include the image inside what it renders.

  20. Hey there, It´s been a while back when I found your lazy loading solution. We did a relaunch and after that I had the time to get my hands back on the lazy loading thing. I got it to work, but I still have one little issue. the starpage is elementor based, and all the images do not load when your script is active. Any ideas about that? I guess its because elementor does lazy loading on its own, and there is something conflict between your script and them. Checking the console get I see the following: data-data-srcset So there is a double data genereated. Any ideas to get this working?

    • Hi Bjoern,

      Since WP 5.5, the system automatically added native lazy loading with the attribute loading="lazy". So it's better to use that than this custom code.

      Elementor probably also using data-srcset, try disabling my PHP code that does that.

  21. thank you for the tip, I just tried, but I have some problem, how to exclude an entire page or just an image (url of the image)?

    • Hi Julien, It takes a major revamp on the REGEX to exclude some images. But it's easy to exclude an entire page. Just add a condition in the add_filter like:

      // Abort if in Page 
      if (is_singular('page')) {
        return $content;
      }
      
      ...

      But as mentioned in the first paragraph, this method is not recommended anymore. Most browsers already support <img loading="lazy"> attribute.

      All images added via Gutenberg automatically have this attribute. But if you use shortcode or ACF Block for the image, it won't have that attribute.

      • Hi Henner,

        Thank you for your answer, but I did not understand to exclude a single image? Do you have a code? Because I have a problem with my logo, it's the 1st image that is loaded after lazy and the logo "jumps" when loading, I would like the logo to be excluded from the lazy or loaded first (preload ?)

        • Hi Julien, Sorry, but we don't have a code to exclude a single image. Ideally, the logo should be outside the Gutenberg's content so it's not affected by the filter

  22. It is very nice code I am using on a client website, but issue is slider I am using slick slider, please help me how can implement in slider