Originally published at: https://roots.io/guides/how-to-use-lozad-js-in-sage/
Lozad.js is a lightweight lazy-loading library that’s just 535 bytes minified & gzipped. It is written with an aim to lazy load images, iframes, ads, videos or any other element using the recently added Intersection Observer API with tremendous performance benefits. Install yarn add lozad Import and add JS Open resources/assets/scripts/routes/common.js and add the import…
Excellent - thank you.
Thanks. If you’re also using the srcset attribute your image tag will look like this:
<img class="lozad" data-src="path-to-src.jpg" data-srcset="..." />
Hi,
A few days ago I decided to use lodaz in a wordpress site i’m developping, and while I was asking myself what could make a great placeholder before images load, I tought it would be nice to leverage wordpress thumbnails feature for that.
The idea is to register a low resolution thumbnail size, in my case 64x48 (around 1~2kb per image), and display a blurred version of it before lodaz does its job and replace it with full quality image version. Then fade the blur out. It would make a kind of Medium-style lazy load.
The final result looks like this:
So first register the low res thumbnail size:
add_image_size('low-res', 64, 48, false);
Then, I use a regex filter function to replace original image codes:
function lazy_images($content)
{
//-- Change src/srcset to data attributes, and replace img src with its low res version
$content = preg_replace_callback(
'/<img(.*?)(src=)\"(.*?)\"(.*?)(srcset=)\"(.*?)\"(.*?)>/i',
function ($matches) {
return "<img$matches[1]$matches[2]" .low_res_image_src($matches[3]) . " " . "data-$matches[2]$matches[3]$matches[4]data-$matches[5]\"$matches[6]\"$matches[7]>";
},
$content
);
// wrap images in a .lazy-wrap div for clean and unblurred borders. Transfer alignment class to wrapping div
$content = preg_replace('/(<img|<iframe)(.*?)(align\w*)([^>]*>)/i', '<div class="lazy-wrap $3">$1$2$4</div>', $content);
//-- Add .lazy class to each image that already has a class.
$content = preg_replace('/(<img|<iframe)(.*?)class=\"(.*?)\"(.*?)>/i', '$1$2class="$3 lazy"$4>', $content);
//-- Add .lazy class to each image that doesn't already have a class.
$content = preg_replace('/(<img|<iframe)((?:(?!class=).)*?)>/i', '$1 class="lazy"$2>', $content);
return $content;
}
This filter is applied to content, widgets, and thumbnails:
add_filter('the_content', 'lazy_images');
add_filter('widget_text', 'lazy_images');
add_filter('post_thumbnail_html', 'lazy_images');
What we need next is the low_res_image_src function that will return the image low res version url:
function low_res_image_src($image_url)
{
//Call the get_attachment_id that will return the id based on image url. Here a slight adjustment may be required to get rid of domain in url
$iid = get_attachment_id(WP_HOME . $image_url);
return wp_get_attachment_image_src($iid, 'low-res')[0];
}
function get_attachment_id($url)
{
$attachment_id = 0;
$dir = wp_upload_dir();
if (false !== strpos($url, $dir['baseurl'] . '/')) { // Is URL in uploads directory?
$file = basename($url);
$query_args = array(
'post_type' => 'attachment',
'post_status' => 'inherit',
'fields' => 'ids',
'meta_query' => array(
array(
'value' => $file,
'compare' => 'LIKE',
'key' => '_wp_attachment_metadata',
),
)
);
$query = new WP_Query($query_args);
if ($query->have_posts()) {
foreach ($query->posts as $post_id) {
$meta = wp_get_attachment_metadata($post_id);
$original_file = basename($meta['file']);
$cropped_image_files = wp_list_pluck($meta['sizes'], 'file');
if ($original_file === $file || in_array($file, $cropped_image_files)) {
$attachment_id = $post_id;
break;
}
}
}
}
return $attachment_id;
}
Add the required CSS (you might want to inline it in your HTML):
.lazy-wrap {
overflow: hidden;
width: fit-content;
}
img.lazy {
transition: filter 0.3s ease;
filter: blur(10px);
//we could also add "transform : scale(1.1)" to avoid white borders around blurred image
}
img.lazy.is-loaded {
filter: blur(0);
}
And finally, we can initialize our lodaz script:
lozad(".lazy", {
rootMargin: "500px 0px",
loaded: function (el) {
el.classList.add("is-loaded");
}
}).observe();
Should this be modified to work for srcset
attributes output by wp_get_attachment_image
?
Edit, like:
if ($attr['src']) {
$attr['data-src'] = $attr['src'];
unset($attr['src']);
}
if ($attr['srcset']) {
$attr['data-srcset'] = $attr['srcset'];
unset($attr['srcset']);
}
Did someone already found a solution to filter the output from Gutenberg?
The filter wp_get_attachment_image_attributes
does not seem to work here.
Hi there – Any idea how we can default this to lozad all images?
Unfortunately, the WP produced “wp-block-cover” header images that come straight out of Sage are DIV backgrounds… Do we append a class to this header somehow, or do we just default all images/iframes regardless?
Many thanks!
This is awesome. Is it possible however to edit the regex to ignore any images that already have a data-src
attribute? For example I have a Slick slider which is lazy-loading images itself, which are then broken by this regex…
I think LucasDemea’s content filtering solution is suitable for Gutenberg. I believe there is no Gutenberg replacement for wp_get_attachment_image yet.
Youtube is a big PageSpeed killer these days but a simple modification to your code seems to solve that:
//-- Replace iframe src with data-src
$content = preg_replace('/(<iframe)(.*?)src=\"(.*?)\"(.*?)>/i', '$1$2data-src="$3"$4>', $content);
Thanks for your work on this!
Problems with lazy-wraps:
Seems to be a problem with the ‘<div class="lazy-wrap’ part. I don’t see any of these wrappers in my markup.
The regex works if I remove align and update the matches, though it may cause issues with align:
$content = preg_replace('/(<img|<iframe)(.*?)([^>]*>)/i', '<div class="lazy-wrap">$1$2$3</div>', $content);
Big Performance Increase:
I noticed that pages that contain a lot of images were struggling with this code, and I pin pointed it to the WP Queries that are called in get_attachment_id(). All of these separate queries have a big hit on server performance and TTFB.
So, to avoid these queries I am simply adding the attachment id into the markup using a filter:
/**
* Add attachment id classes to images to enhance lazy loading performance
* https://letswp.io/add-attachment-id-to-the-class-of-wordpress-images/
*/
add_filter( 'wp_get_attachment_image_attributes', function ( $attr, $attachment ) {
$class_attr = isset( $attr['class'] ) ? $attr['class'] : '';
$has_class = preg_match(
'/wp\-image\-[0-9]+/',
$class_attr,
$matches
);
// Check if the image is missing the class
if ( !$has_class ) {
$class_attr .= sprintf( ' wp-image-%d', $attachment->ID );
// Use ltrim to to remove leading space if necessary
$attr['class'] = ltrim( $class_attr );
}
return $attr;
}, 10, 2);
We can then modify low_res_image_src() like so:
function low_res_image_src($image_url, $image_classes)
{
// If we have the attachment id in the image then we can save on queries
preg_match('/wp-image-([^"]*)/', $image_classes, $matches);
if($matches) {
$iid = $matches[1];
}
else {
$iid = get_attachment_id(WP_HOME . $image_url);
}
And in lazy_images() we would pass the img classes as an additional parameter:
low_res_image_src($matches[3], $matches[4])
With these modifications my page speed score on gallery pages went from 60s to 90s.
Thanks @stuartcusackie for your upgrade on this.
I was trying to use your code and was struggling figuring out why it didn’t work… That’s because the wp_get_attachment_image_attributes filter is only applied to featured images.
So this solution will work in some cases but unfortunately not in every one
Thanks for the heads up! I’m almost sure that this used to work on all images, perhaps a recent change in the WP filters?
If I ever find a solution I will add it here.