WordPress Theme Image LazyLoad Tutorial With Adaptive Height Placeholders

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, 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

<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

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);
?>

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>

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.

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.

Comments


Post A Comment

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