WordPress Theme Image LazyLoad Tutorial With Adaptive Height Placeholders

Article Last Updated: 29 Dec 2020

This post uses traditional approach to achieve image lazy loading with JavaScript (and then some CSS and PHP). Since August 2019 and Chrome/Chromium 76+ (or newer) you can use browser’s built-in native lazy loading support. Still, this JavaScript solution is really robust and cross-browser compatible (including older ones in particular). LazySizes.js also has a native-loading plugin, so that it can automatically fallback to classic javascript version if no support is found in browser. More information about native support can be found here.

We’ve implemented LazySizes library on our website, and it is working great! In return, you will receive higher PageSpeed and Lighthouse scores, respectively, which is important SEO optimization. However, one negative side-effect surfaced: text reflow on scroll / image loading (CLS = Cumulative Layout Shift), as the dummy 1×1 pixel placeholder gets replaced with the actual image (which is annoying to say at least).

Now, we wouldn’t have any issues if the image thumbnails were static, fixed in size and non-responsive. It is relatively easy to keep scaled/resized image aspect ratio in CSS, but how do you set a universal placeholder to behave exactly the same? Blog is not a thing set in stone, articles are not typical product-like pages with template galleries! Unfortunately, this cannot be done purely in CSS yet (there is no support for AR), and some CSS+JS combination is required.

Lazy Load Responsive Adaptive Image Placeholders Height Implementation Code

This tutorial uses already mentioned LazySizes library, but you can try anything else, really (with appropriate code modifications). It is versatile and very easy to implement in WordPress theme, for example. In default setup, simply include the library in the footer/header, and append lazyload class to your target images – plugin does the rest automatically.

One important thing to note with LazySizes JS Library is that it processes all images with .lazyload class present. During the loading of original images, that class is removed and replaced with temporary .lazyloading class. Finally, once the loading of original image is completed, .lazyloading is replaced with .lazyloaded class. We exploit all 3 classes in our CSS later!

LazySizes.js Image Classes

1. requires .lazyload class @ image placeholders
2. appends/replaces .lazyload with .lazyloading class during image loading process
3. appends/replaces .lazyloading with .lazyloaded class when image is finished loading

HTML @ Header

<!-- LazySizes.js -->
<script src="lazysizes.min.js" async=""></script>
<!-- LazySizes.js Hide Image Lazy Load Placeholder @ nojs -->
<noscript><style>img.lazyload{display:none !important;}</style></noscript>

HTML @ Body

this is example only – it is completely generated on server-side

<div id="attachment_xyz" class="image-wrapper">
	<a href="" target="_blank" rel="noopener">
		<img class="lazyload" src="" alt="" data-src="" data-srcset="" width="X" height="Y" aria-label="" />
		<noscript>
		<img src="" alt="" width="X" height="Y" srcset="" aria-label="" />
		</noscript>
	</a>
</div>

CSS

selectors are generic / universal, adapt them to target article / blog posts content only

div[id^='attachment_'] {
	display:block;
	margin:1% auto;
	padding:1%;
	width:96% !important;
	height:100%;
	overflow:hidden;
	box-sizing:border-box;
}

@media(max-width:768px) {
	div[id^='attachment_'] {
		display:block;
		width:100% !important;
		height:100% !important;
		margin:0;
		padding:0;
	}
}

/* generic | noscript | no lazyload */
img {
	display:block;
	width:auto;
	height:auto;
	margin:5px auto;
	border:2px solid #EEE;
	box-sizing:border-box;
	background-color:#FFF;
	background-repeat:no-repeat;
	background-image:none;
}

/* lazyloading placeholder */
img.lazyload,
img.lazyloading {
	width:auto;
	height:auto;
	background-color:#EEE;
}

/* lazyloading finished */
img.lazyloaded{
	width:auto !important;
	height:auto !important;
}

@media(max-width:768px) {
	/* generic | noscript | no lazyload */
	img {
		display:block;
		width:100%;
		margin:0 auto;
		box-sizing:border-box;
	}

	/* lazyloading placeholder */
	img.lazyload,
	img.lazyloading{
		width:100%;
		background-color:#EEE;
	}

	/* lazyloaded finished */
	img.lazyloaded{
		width:100% !important;
		height:auto !important;
	}
}

PHP @ functions.php

<?php
###################
# IMAGE LAZY LOAD #
###################
## HTML REPLACEMENT FUNCTIONS
function imagelazyload_preg_replace_html($content, $tags) {

	// define i/o containers
	$search = array();
	$replace = array();

	// attributes to search for
	$attrs_array = array('src','srcset','sizes');

	// elements requiring 'src' attribute to be valid HTML
	$src_req = array('img','video');

	// loop through $tags
	foreach($tags as $tag) {
		// is the tag self closing?
		$self_closing = in_array($tag, array('img','embed','source'));

		// set tag end depending on if it's self-closing
		$tag_end = ($self_closing) ? '\/' : '<\/'.$tag;

		// look for img tag in content
		preg_match_all('/<'.$tag.'[\s\r\n]([^<]+)('.$tag_end.'>)(?!<noscript>|<\/noscript>)/is',$content,$matches);

		$n=1;
		$countMatches = count($matches[0]);

		// if tags exist loop through $matches[] array and perform regex search & replace
		if ($countMatches) {
			foreach ($matches[0] as $match) {

				// skip first img element | do not lazyload first img element
				if ($n > 1) {

					// set original version for <noscript>
					$original = $match;

					// append original into $search array
					$search[] = $original;

					// look for img class attribute
					$img_classes = preg_match('/[\s\r\n]class=[\'"](.*?)[\'"]/', $match, $classes);

					// extract img classes
					if (isset($classes[1])) {
						$img_classes_extract = $classes[1];
					} else {
						$img_classes_extract = '';
					}

					// bypass* lazy load elements with special class
					$noLazyClassPattern = 'nolazyload';

					if (strpos($img_classes_extract, $noLazyClassPattern) === FALSE) {

						// insert lazy load class
						$img_classes_insert = $img_classes_extract . ' ' . 'lazyload';

						// replace img class @ $match
						$match = str_replace($img_classes_extract, $img_classes_insert, $match);

						// if element requires 'src'

						// set src container
						$src = '';

						// option #1: set src to default image loader.gif
						##$src = (in_array($tag, $src_req)) ? ' src="'.get_template_directory_uri().'/images/loader.gif"' : '';
						// option #2: set src to default transparent image
						$src = (in_array($tag, $src_req)) ? ' src="'.get_template_directory_uri().'/images/1x1-transparent.png"' : '';
						// option #3: remove src default blank image space holder
						##$src = (in_array($tag, $src_req)) ? ' ' : '';

						// set replace html container
						$replace_markup = $match;

							##################
							# REGEX EXAMPLES #
							##################
							# src => data-src (for lazysizes.js library)
							// $replace_markup = preg_replace('/[\s\r\n]('.$attrs.')?=/', $src.' data-$1=', $replace_markup);
							# src => data-echo (for echo.js library)
							// $replace_markup = preg_replace('/[\s\r\n]('.$attrs.')?=/', $src.' data-echo=', $replace_markup);

						// replace attr with data-attr

						// set replacement counter | insert replacement src @ first tag only | prevents cascading replacement of src tags
						$i=1;
						foreach ($attrs_array as $attr) {
							if ($i==1) {
								$replace_markup = preg_replace('/[\s\r\n]('.$attr.')?=/', $src.' data-$1=', $replace_markup);
							} else {
								$replace_markup = preg_replace('/[\s\r\n]('.$attr.')?=/', ' data-$1=', $replace_markup);
							}
							$i++;
						}

						// add original html markup in as <noscript>
						$replace_markup .= '<noscript>'.$original.'</noscript>';

						// append replacement into $replace array
						$replace[] = $replace_markup;

					} else {
						// append original* into $replace array
						$replace[] = $original;
					}

				}

				$n++;

			}
		}
	}

	// replace all $search items with $replace items
	$newcontent = str_replace($search, $replace, $content);
	return $newcontent;
}

function imagelazyload_filter_html($content) {
	// feed bypass
	if (is_feed()) {
		return $content;
	}

	// amp page bypass
	if (is_amp_page()) {
		return $content;
	}

	// opera mini bypass
	$useragent = (isset($_SERVER['HTTP_USER_AGENT'])) ? $_SERVER['HTTP_USER_AGENT'] : '';
	if (stripos($useragent, 'Opera Mini') == TRUE) {
		return $content;
	}

	// set new content
	$newcontent = $content;

	// replace img 'src' with 'data-src' attribute
	$newcontent = imagelazyload_preg_replace_html($newcontent, array('img'));

	// return modified content
	return $newcontent;
}

## HTML REPLACEMENT FILTERS
// replace 'src' in the_content
// priority = 100 is important in order to process full the_content() composite and prevent html structure distortion (namely, <p> caption tags)!
add_filter('the_content', 'imagelazyload_filter_html', 100);
?>

PHP does a massive job here, providing core HTML processing (static caching recommended) on-the-fly, of course. It replaces images in posts and provides crucial image data which we later cleverly use in frontend through JavaScript to build our placeholders with dynamic width and height. Also, it provides bypass for WordPress AMP plugin. Also, if you wish to bypass lazyloading for certain media files, simply append nolazyload class to it’s source code and it will be skipped.

JavaScript (jQuery) @ footer

<script type="text/javascript">
function lazyLoadPlaceholders() {
	let Images = $("img.lazyload");
	for (let i = 0; i < Images.length; i++) {
		let W = $(Images[i]).attr('width');
		let H = $(Images[i]).attr('height');
		let R = W/H;
		let Width = $(Images[i]).width();
		let Height = $(Images[i]).height();

		if (window.innerWidth < 768) {
			$(Images[i]).width('100%');
			$(Images[i]).height(Width/R);
		} else {
			$(Images[i]).width(W);
			$(Images[i]).height(W/R);
		}
	}
}
$(document).ready(function() {
	lazyLoadPlaceholders();
});
$(window).resize(function() {
	lazyLoadPlaceholders();
});
</script>

In JS we’ve added resize window event support, thus, when user resizes desktop browser window size (less likely) or rotates mobile device in landscape or portrait position (much more likely), placeholders will be dynamically recalculated and scaled appropriately. For this reason, we also had to add CSS overrides once lazyloading is done, avoiding squashed images.

UPDATE TIP: pure CSS only aspect ratio based smart image placeholders

While JavaScript (jQuery) code to create preloading image placeholders presented above is simple, fast and reliable, with a wide range of older browsers support, you can now use a modern CSS-only approach to create aspect-ratio locked image placeholders, as an alternative to traditional javascript solution. Modern browsers can now set default aspect ratio of images based on image’s width and height attributes (remember, WP PHP stage already provides the necessary attributes for us!):

CSS

img {
	aspect-ratio:attr(width) / attr(height);
}

Keep in mind that this solution might not always work out-of-the-box and may require additional tweaks in nested elements, which can still cause a reflow to occur.

Comments


Post A Comment

I have read and consent to Privacy Policy and Terms and Conditions