22 Nov 2020

WordPress full Image Lazy Loading solution including the images from the WYSIWYG backend editor.

Encountering content jumps may be very frustrating. Especially if your scroll event is being tracked and the size of your Document doesn’t get updated after an element got loaded into the DOM.

It’s pretty easy to handle images in your template and manipulate them to behave as expected. But what to do with the WYSIWYG images?

Let’s prepare our Markup

<div class="lazy-container" style="padding-bottom: image_ratio%;">
   <img data-src="image_path" />
</div>

The tricks are:

  1. Absolutely position the image in the wrapper
  2. The wrapper height should be defined via padding-bottom and the image’s ratio
  3. The image ratio (as a percentage) can be calculated like this:  (image_height / image_width * 100)

This way we assure the wrapper inherits images height although the image is not in the DOM yet. Obviously we need the height and the width of the image, but wordpress delivers this data. We will take care of that later.

The styles are as follows:

.lazy-container {
  position: relative;
}

.lazy-container img {
  position:absolute;
  top:0;
  left:0;
  max-width: 100%;
}

.lazy-container:after {
  background-color: #eee;
  content: "";
  display: block;
  width: 100%;
  height: 100%;
  position: absolute;
  top: 0;
  left: 0;
  background-image: url("data:image/svg+xml;base64,PCEtLSBCeSBTYW0gSGVyYmVydCAoQHNoZXJiKSwgZm9yIGV2ZXJ5b25lLiBNb3JlIEAgaHR0cDovL2dvby5nbC83QUp6YkwgLS0+Cjxzdmcgd2lkdGg9IjM4IiBoZWlnaHQ9IjM4IiB2aWV3Qm94PSIwIDAgMzggMzgiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgc3Ryb2tlPSIjYWFhIj4KICAgIDxnIGZpbGw9Im5vbmUiIGZpbGwtcnVsZT0iZXZlbm9kZCI+CiAgICAgICAgPGcgdHJhbnNmb3JtPSJ0cmFuc2xhdGUoMSAxKSIgc3Ryb2tlLXdpZHRoPSIyIj4KICAgICAgICAgICAgPGNpcmNsZSBzdHJva2Utb3BhY2l0eT0iLjUiIGN4PSIxOCIgY3k9IjE4IiByPSIxOCIvPgogICAgICAgICAgICA8cGF0aCBkPSJNMzYgMThjMC05Ljk0LTguMDYtMTgtMTgtMTgiPgogICAgICAgICAgICAgICAgPGFuaW1hdGVUcmFuc2Zvcm0KICAgICAgICAgICAgICAgICAgICBhdHRyaWJ1dGVOYW1lPSJ0cmFuc2Zvcm0iCiAgICAgICAgICAgICAgICAgICAgdHlwZT0icm90YXRlIgogICAgICAgICAgICAgICAgICAgIGZyb209IjAgMTggMTgiCiAgICAgICAgICAgICAgICAgICAgdG89IjM2MCAxOCAxOCIKICAgICAgICAgICAgICAgICAgICBkdXI9IjFzIgogICAgICAgICAgICAgICAgICAgIHJlcGVhdENvdW50PSJpbmRlZmluaXRlIi8+CiAgICAgICAgICAgIDwvcGF0aD4KICAgICAgICA8L2c+CiAgICA8L2c+Cjwvc3ZnPg==");
  background-repeat: no-repeat;
  background-position: center center;
  background-size: 10%;
  transition: all 0.3s;
}

We use a pseudo selector to style the placeholder in order to make an animation possible.

Here is the JS to manipulate the src and the css
(note: we will use waypoints to track if elements arrive into viewport)

import "waypoints/lib/noframework.waypoints";

const images = document.querySelectorAll(".lazy");

images.forEach((image) => {
  new Waypoint({
    element: image,
    handler: function (direction) {
      handleLazyItem(image);
      this.destroy(); // let's get rid of the waypoint after the image got loaded
    },
    offset: "50%"
  });
});

const handleLazyItem = (image) => {
  // handle the src attribute
  const src = image.getAttribute("data-src");
  image.setAttribute("src", src);
  
  // handle the css of the container
  const container = image.parentElement;
  container.classList.add("lazy-container--loaded");
};

So everything wrapped together in a code sandbox looks like this:

WordPress Template

Ok so for the php templates the solution is straight forward: let’s take a featured image as an example and also calculate the ratio mentioned above.


$image_id = get_post_thumbnail_id( $post->ID );
$image_obj = wp_get_attachment_image_src( $image_id, 'medium' );
$image_width = $image_obj[1];
$image_height = $image_obj[2];
$image_src = $image_obj[0];
$image_ratio = $image_height/$image_width * 100;

<div class="lazy-container" style="padding-bottom: <?php echo $image_ratio; ?> %;">
   <img data-src="<?php echo $image_src; ?>" />
</div>

WordPress post images from the_content()

This is where the whole thing gets interesting. We need to manipulate the_content() and adjust the <img> to fit our markup.

This is the php snippet you can put into your functions.php and see the magic happens. Have fun!!!

add_filter('the_content', 'change_image_markup', 15); 
function change_image_markup($the_content) {
      
   //don't run on the backend
   if( is_admin()) return $the_content;

   $post = new DOMDocument();
   libxml_use_internal_errors(true);
   $post->loadHTML('' . $the_content);
   $figures = $post->getElementsByTagName('figure');

     // Iterate each img tag
     foreach( $figures as $figure ) {
        
        if ($figure->getAttribute('class') === 'wp-block-audio' || $figure->getAttribute('class') === 'wp-block-video') continue;

        $img = $figure->firstChild;
        $imgClass = $img->getAttribute('class');
        $imgId = preg_replace('/[^0-9]/', '', $imgClass);

        //size  
         $size = 'full';
         $figureClass = $figure->getAttribute('class');
         $figureClassArr = explode(' ', $figureClass);
         foreach($figureClassArr as $item){
            if (strpos($item, 'size') !== false) {
               $position = strpos($item, "-") + 1;
               $size = substr($item, $position);
               break;
            }
         }      

        $thumbnail_obj = wp_get_attachment_image_src( $imgId, $size );
        
        //if SVG lets break the loop
        $thumbnail_type= get_post_mime_type($imgId);
        if($thumbnail_type == 'image/svg+xml') return $post->saveHTML();

        $thumbnail_width = $thumbnail_obj[1];
        $thumbnail_height = $thumbnail_obj[2];
        $thumbnail_url = $thumbnail_obj[0];
        $thumbnail_ratio = $thumbnail_height/$thumbnail_width * 100;
        $wrapper = $post->createElement('div');
        $wrapper->setAttribute('class','lazy-container');
        $wrapper->setAttribute('style', 'padding-bottom:'. $thumbnail_ratio .'%');

        $imgClone = $img->cloneNode();
        $imgClone->removeAttribute('class');
        $imgClone->removeAttribute('src');
        $imgClone->setAttribute('class', $imgClass . ' lazy');
        $imgClone->setAttribute('data-src', $thumbnail_url);

        $wrapper->appendChild($imgClone);
        $figure->replaceChild($wrapper, $img);
     };
     libxml_clear_errors();

     return $post->saveHTML();
}

See all posts