How to make high loading speed and SEO optimization of images on the site using CMF MODX Revolution

How to make high loading speed and SEO optimization of images on the site using CMF MODX Revolution

Optimization of images positively affects one of the important factors of website ranking in search - page load speed. In this article, let's see how to make automatic optimization and compression of images on a site running CMF MODX Revolution. Also recommendations on microdata and lazy loading.

Optimization Steps

I will write a more detailed article about optimizing images on the site. Let's briefly describe the stages of optimization once again.

Resolution

The picture should have approximately the same resolution as it occupies on the screen. Otherwise it is rendered by the browser in a reduced size, and a higher resolution does not give any advantages. but the weight of the file remains high and the user is forced to download it.

As you can see in the example above, the file weight increases faster than the image size. This is because both vertical and horizontal pixels are added.

Compression

According to Google Pagespeed documentation, images with a compression level of more than 85 are considered optimized. We need to compress our files to this level.

Format

Different formats are suitable for different purposes. Briefly:

  • SVG - vector, for icons
  • PNG - raster, with transparency, weighs a lot, good support for browsers.
  • JPG - raster, without transparency, lightweight, good support for browsers.
  • WEBP - raster, with transparency, light weight, medium browser support.

In most cases WEBP+JPG will be enough. WEBP is a modern format from Google that preserves transparency and optimizes file weight well, even better than JPG.

The problem is that it is not supported in all older browsers. You can check at caniuse.com:

WEBP format accessibility statistics in browsers by caniuse service

Unlike PNG, which is supported by almost all browsers:

PNG format accessibility statistics in browsers by caniuse service

So we use JPG or PNG - whether we need transparency or not. And add WEBP as an alternative format. If the browser supports it, it will choose WEBP.

Adaptability

Usually, the internet on mobile is worse than on desktops. At the same time, images take up smaller space on the screen. Therefore, it is wrong to load the same image - it is right to load different variants.

Also, a website template can be built in such a way that a large image at 100% of the screen width is loaded on mobile, and a small 100px width thumbnail preview is loaded on desktop.

You need to research your site on different devices and understand for which screen sizes you need to resize the image.

Screen resolutions

On Apple devices, and on many analogs of premium segments, the pixel density of the screen is often higher than standard. To make the picture not look blurry on such screens, you need to set an alternative picture with a higher resolution.

Schema.org microdata

The page contains many temporary files of optimized images. We don't want to lose potential traffic from image search services, so we need to provide a link to the original file. In addition, Schema.org markup of images specifically as an Image object will potentially improve SEO. Images will be able to get into the page's snippet in the search engine.

Through the meta tag we can not pass a link to the image file, so I decided to do it using the tag a. I did not find any disadvantages of this method. We will hide the link, but the search engine will scan it.

Just add to the wrapper block that it is a Image type object. And inside we will place a hidden by display:none and rel=nofollow. Let's mark that it passes the contentUrl value. Let's add a domain to the link so that it is absolute. I think this is more correct than a relative link in microdata.

<div class="article-image__wrapper" itemscope itemtype="https://schema.org/ImageObject">
	<a itemprop="contentUrl" href="[[++site_url]][[+src]]" style="display:none;" rel="nofollow"></a>	
	<picture>	
		<source 
		    media="(min-width: 550px)"
			srcset="..." 
			type="image/jpeg">
		<img class="lazyload"
			loading="lazy" decoding="async"
			src="..."
			alt="[[+alt]]">
	</picture>
</div>

Lazy Load

First, let's set the link to the image in the data-src attribute. Then add a special JS script waiting for the scroll event. If the picture is close to the user's viewport, the script substitutes the link from data-src into src. Then the browser starts loading the file.

There are several libraries for lazy loading. I liked the code of this project on Github for this purpose. It is written in native JavaScript and works correctly with adaptive images in the picture tag, which is what we need.

But there is a nuance - since initially we display the picture empty, it will occupy an unknown height. If you don't do anything with it, the height of the page will change when you scroll the page, which is not very comfortable.

Summarized algorithm of actions:

  • add the Lazy Load JS script to the page;
  • add lazyload class to the images;
  • set the height and width of the image in the chunk. For this we will pass the image to placeholders;
  • after loading the content, run a script that will detect the real width of the picture and proportions of its size, and set its real height;
  • add styles for 404 images so that they occupy a small height if the image is not loaded;
  • add height change to auto after loading to the lazy loading script;
    in the lazyload script, fix that the lazyload class is removed from the processed image at once. Otherwise, if the picture is not loaded (wrong address) - it caused cyclic loading and flickering in my case. Not nice. But in essence, what's the difference - the picture is loaded, the script has done work on it and can't do anything else if the address is specified incorrectly.

Now let's see how to realize all this in MODX.

MODX Tools

Basics

For resizing and compressing images, there is a wonderful snippet called pthumb. To use it, install the package with the same name in the installer. Following the recommendations above, we need to use code like this:

<picture>
	<source 
		media="(min-width: 567px)" 
		srcset="[[pthumb?input=`examle-image.jpg`&options=`w=800&f=webp&dpi=72`]],
		[[pthumb?input=`examle-image.jpg`&options=`w=1600&f=webp&dpi=72`]] 2x" 
		type="image/webp" alt="[[+alt]]">
	<source 
		media="(min-width: 567px)" 
		srcset="[[pthumb?input=`examle-image.jpg`&options=`w=800&f=jpg&q=85&dpi=72`]],
		[[pthumb?input=`examle-image.jpg`&options=`w=1600&f=jpg&q=85&dpi=72`]] 2x" 
		type="image/jpeg" alt="[[+alt]]">
	<source 
		media="(max-width: 566px)" 
		srcset="[[pthumb?input=`examle-image.jpg`&options=`w=450&f=webp&dpi=72`]],
		[[pthumb?input=`examle-image.jpg`&options=`w=800&f=webp&dpi=72`]] 2x" 
		type="image/webp" alt="[[+alt]]">
	<img 
		src="[[pthumb?input=`examle-image.jpg`&options=`w=450&f=jpg&q=85&dpi=72`]]"
		srcset="[[pthumb?input=`examle-image.jpg`&options=`w=800&f=jpg&q=85&dpi=72`]] 2x" 
		alt="[[+alt]]">
</picture>

Note that compression does not work for WEBP format images. We can get around this by specifying a smaller resolution - for example, if the real size is 400px, we would set 400*0.85=340.

The picture tag specifies several alternative sources of the file. If the browser is old and does not support this feature, it will load an image from the img tag, so it must be specified, and of PNG or JPG format.

  • The new browsers will load images with media="(min-width: 567px)" on mobile devices, and larger versions for desktops. 
  • For different pixel densities, we set alternative larger images with 2x density - they will be loaded for screens with double or higher density. You can also set 1.5x and 3x.

In the pthumb options parameter, we set the width of the picture in the w parameter, without specifying the height (h parameter). So the height will be calculated automatically. This option is suitable if the images in the template may differ in height. These settings depend on your template - maybe you are limited by height.

If you need a specific aspect ratio, set the h parameter and at the same time the zc=1 parameter. If you do not set it, the image will have margins. If you set it, the picture will be adjusted by height or width, and the excess will be cropped.

For example, for product cards with transparent backgrounds, it is better not to specify it, so that the product image is not cropped. But for article previews, it is possible.

This code can be used in chunks or page templates. Change the call parameters for specific tasks. Remember that in the tag img should be a picture JPG or PNG, so that in old browsers tag picture worked correctly, and in srcset you can specify WEBP.

But, for example, for blog articles, this method is not very convenient. 
I want to insert an image in the visual editor, but I want it to be optimized automatically when the page loads. Let's consider this option.

Automatize

We will use the same pthumb as a snippet for optimization. First we need to create a chunk with markup of the image block, how it should look after optimization. Let's call it tpl.Img.

<div class="article-image__wrapper">
	<picture>
		<source 
			media="(max-width: 566px)" 
			srcset="[[phpthumbon?input=`[[+src]]`&options=`w=450&f=webp&q=85&dpi=72`]],
			[[phpthumbon?input=`[[+src]]`&options=`w=800&f=webp&q=85&dpi=72`]] 2x" 
			type="image/webp" alt="[[+alt]]">
		<source 
			media="(min-width: 567px)" 
			srcset="[[phpthumbon?input=`[[+src]]`&options=`w=800&f=webp&q=85&dpi=72`]],
			[[phpthumbon?input=`[[+src]]`&options=`w=1600&f=webp&q=85&dpi=72`]] 2x" 
			type="image/webp" alt="[[+alt]]">
		<source 
			media="(min-width: 567px)" 
			srcset="[[phpthumbon?input=`[[+src]]`&options=`w=800&f=jpg&q=85&dpi=72`]],
			[[phpthumbon?input=`[[+src]]`&options=`w=1600&f=jpg&q=85&dpi=72`]] 2x" 
			type="image/jpeg" alt="[[+alt]]">
		<img 
			loading="lazy" decoding="async"
			src="[[phpthumbon?input=`[[+src]]`&options=`w=450&f=jpg&q=85&dpi=72`]]"
			srcset="[[phpthumbon?input=`[[+src]]`&options=`w=800&f=jpg&q=85&dpi=72`]] 2x" 
			alt="[[+alt]]">
	</picture>
</div>

Now we need to create and configure a plugin that will run before page rendering and replace all img tags in the content to call the optimized image chunk. During rendering this chunk will be processed by MODX parser and we will get better optimized images.

Create plugin pl.ImgAutoOptimize (you can call it something else) with this code:

if ($modx->event->name != "OnLoadWebDocument" || $modx->context->key == 'mgr') {
   return;
}

// Get page content
$output = $modx->resource->get('content');

// Function replace img tags to chunk with parameters
function optimage($img) {
	$src = preg_replace('/.* src="(.*?)".*/m', '$1', $img[0]);
	$alt = preg_replace('/.* alt="(.*?)".*/m', '$1', $img[0]);
	$desc = preg_replace('/.* title="(.*?)".*/m', '$1', $img[0]);
	
	// fix - preg_replace returns the full tag if no matches are found
	// set it to empty
	if ($src == $img[0]) {
		$src = '';
	}
	if ($alt == $img[0]) {
		$alt = '';
	}
	if ($desc == $img[0]) {
		$desc = '';
	}
	
	return '[[$tpl.Img?src=`'.$src.'`&alt=`'.$alt.'`&desc=`'.$desc.'`]]';
}

// Replace img
$output = preg_replace_callback('/(<p>\s*|)<img[^>]*>(\s*<\/p>|)/m', 'optimage', $output);

// Set content
$modx->resource->set('content', $output);

Set the plugin to run on the OnLoadWebDocument system event.

The plugin finds all img tags in the resource content, selects src, alt and title attribute values from them. It replaces img with the call of the chunk tpl.Img, which contains the corresponding placeholders. The title value in the chunk can be displayed as an image caption, for example.

[[+desc:notempty=`<p class='article-img-desc'>[[+desc]]</p>`]]

Summary

So, the example image chunk is like this (with micropartitioning):

<div class="article-image__wrapper"  itemscope itemtype="https://schema.org/ImageObject">
	<a itemprop="contentUrl" href="[[++site_url]][[+src]]" style="display:none;" rel="nofollow"></a>		
	<picture>
		<source 
			media="(max-width: 566px)" 
			data-srcset="[[phpthumbon?input=`[[+src]]`&options=`w=450&f=webp&q=85&dpi=72`]],
			[[phpthumbon?input=`[[+src]]`&options=`w=800&f=webp&q=85&dpi=72`]] 2x" 
			type="image/webp" alt="[[+alt]]">
		<source 
			media="(min-width: 567px)" 
			data-srcset="[[phpthumbon?input=`[[+src]]`&options=`w=800&f=webp&q=85&dpi=72`]],
			[[phpthumbon?input=`[[+src]]`&options=`w=1600&f=webp&q=85&dpi=72`]] 2x" 
			type="image/webp" alt="[[+alt]]">
		<source 
			media="(min-width: 567px)" 
			data-srcset="[[phpthumbon?input=`[[+src]]`&options=`w=800&f=jpg&q=85&dpi=72`]],
			[[phpthumbon?input=`[[+src]]`&options=`w=1600&f=jpg&q=85&dpi=72`]] 2x" 
			type="image/jpeg" alt="[[+alt]]">
		[[pthumb?input=`[[+src]]`&options=`f=jpg&q=800&dpi=72`&toPlaceholder=`thumb`]]
		<img 
			class="lazyload"
			loading="lazy" decoding="async"
			data-src="[[+thumb]]" data-width="[[+thumb.width]]" data-height="[[+thumb.height]]"
			alt="[[+alt]]">
	</picture>
</div>

Add a script to set the height of the picture:

/* fix height of pictures before lazy loading */
document.addEventListener("DOMContentLoaded", () => {
	var images = document.querySelectorAll("img.lazyload");
	var i = images.length;
	while (i--) {
		/* real width of the picture */
		let width = images[i].offsetWidth; 
		/* proportional real height */
		let height = images[i].dataset.height / images[i].dataset.width * width;
		images[i].style.height = height + 'px';
	}
});

And the lazy loading script, with the modifications I described above. You can customize the offset variable to make the images load earlier or later.

! function() {
    function lazyload() {
        var images = document.querySelectorAll("img.lazyload");
        var i = images.length;
        !i && window.removeEventListener("scroll", lazyload);
        while (i--) {
            var wH = window.innerHeight;
            var offset = 500; /* you can change */
            var yPosition = images[i].getBoundingClientRect().top - wH;
            if (yPosition <= offset) {
                /* Flicker Fix */
                images[i].classList.remove("lazyload");
                images[i].classList.add("loaded");
                /* Delete fixed height */
                images[i].style.height = 'auto';
                if (images[i].getAttribute("data-src")) {
                    images[i].src = images[i].getAttribute("data-src");
                };
                if (images[i].getAttribute("data-srcset")) {
                    images[i].srcset = images[i].getAttribute("data-srcset");
                };
                if (images[i].parentElement.tagName === "PICTURE") {
                    var sources = images[i].parentElement.querySelectorAll("source");
                    var j = sources.length;
                    while (j--) {
                        sources[j].srcset = sources[j].getAttribute("data-srcset");
                    };
                };
                /*  images[i].addEventListener('load', function() {
                     this.classList.remove("lazyload");
                     this.classList.add("loaded");
                }); */
            }
        }
    }
    lazyload();
    window.addEventListener("scroll", lazyload);
}();

Add styles for unloaded images so that they occupy one line in height. We will also write a simple rendering animation for uploaded images.

img[data-width=""] {
    max-height: 2rem;
}

img.loaded {
    animation: fadeIn 600ms ease;
}

@keyframes fadeIn {
    from { opacity: 0; }
    to { opacity: 1; }
}

This can be done directly in the script for quick editing in the future:

let style = document.createElement('style');
style.type = 'text/css';
style.innerHTML = `
	img[data-width=""] {
		max-height: 2rem;
	}	

	img.loaded {
		animation: fadeIn 600ms ease;
	}

	@keyframes fadeIn {
		from { opacity: 0; }
		to { opacity: 1; }
	}
`;
document.head.appendChild(style);

I think that's all you need, now the images on your site will be automatically optimized. Thank you for your attention!

Please rate this article
(0 stars / 0 votes)