How to improve pagespeed performance related to painting images

BB

Optimize the browser tasks related to image painting to improve website loadspeed

Image optimization mostly focuses on controlling when the browser downloads an image and the resolution of the image downloaded.

Dynamically Loading Images

Images with a src="" will automatically download and get painted after the download finishes. One way to control when these downloads and painting jobs occur is by dynamically setting the src tag.

// data-src instead of src
<img data-src="https://images.pexels.com/photos/5321080/pexels-photo-5321080.jpeg?cs=srgb&dl=pexels-kristina-polianskaia-5321080.jpg&fm=jpg" alt="">

A simple script can be used to set the src image attribute based on the value of the data-src attribute.

// Add a 'lazyimage' class to images that should lazyload
const lazyImages = document.getElementsByClassName("lazyimage");

for (let i = 0; i < lazyImages.length; i++) {
  let thisImage = lazyImages[i];
  let thisImageSrc = thisImage.getAttribute("data-src");
  if (thisImageSrc) {
    // Create the src attribute equal to the URL value of the data-src attribute
    thisImage.src = thisImageSrc;
  }
}

There's no restrictions on what image srcs are set, so all images for the page will still download.

Lazyload on scroll with IntersectionObserver

IntersectionObserver can be used to trigger image lazyloading when the user scrolls to that area of the page.

// Add a 'lazyimage' class to images that should lazyload
const lazyImages = document.getElementsByClassName("lazyimage");
const observerOptions = {
  rootMargin: "0px"
}

const imageObserver = new IntersectionObserver(function(entries) {
  for (let i = 0; i < entries.length; i++) {
    let observerObject = entries[i];
    let observedImage = entries[i].target;
    if (observerObject.isIntersecting && observedImage.hasAttribute("data-src")) {
      let thisImageSrc = observedImage.getAttribute("data-src");
      observedImage.src = thisImageSrc;
      // Stop watching an image that has been lazyloaded
      imageObserver.unobserve(observedImage);
    }
  }
}, observerOptions)
for (let i = 0; i < lazyImages.length; i++) {
  let thisImage = lazyImages[i];
  // Add each image observer
  imageObserver.observe(thisImage)
}

Alternatively, use the lazysizes library, which is a lightweight image lazyloading library.

Serving different images based on screen size

Consider on a mobile device the screen size is only around 300 - 500px wide. In many cases, images with a much larger resolution are loaded. They are unnecessarily big for mobile, so the actual displayed size is much smaller than the native resolution of the image. Modern browsers support responsive images markup, which is used to serve a different image size based on the screen size.

<img srcset="some-image-480w.jpg 480w,
             some-image-800w.jpg 800w"
     sizes="(max-width: 600px) 480px,
            800px"
     src="some-image-800w.jpg"
     alt="Some Image Description">

sizes defines a set of media conditions (e.g. screen widths) and indicates what image size would be best to choose, when certain media conditions are true. In this case, before each comma we write:

  • media condition max-width:600px (when the viewport width is 600 pixels or less)
  • The width of the slot the image will fill when the media condition is true (480px)

The image file names are either generated manually or automatically by an image processor on the backend. For example, Wordpress generates various image sizes each time an image is uploaded. The naming convention should match the naming convention used by the image processor.

Responsive images could be used with an image lazyloader by specifying a data-srcset and data-src.

<img
	alt=""
	sizes="(min-width: 1000px) 930px, 90vw"
	data-srcset="small.jpg 500w,
		medium.jpg 640w,
		big.jpg 1024w"
	data-src="medium.jpg"
	class="lazyload" />

Combining Background Image Requests

An image sprite is a collection of images put into a single image.

Using image sprites will reduce the number of network requests. Start off with a background-image that contains multiple images, like icons. Then reference the parent class containing the image sprite. Use a unique class with a background-position set. This controls what aspect of the image gets displayed.

<div id="div1" class="toolbtn"></div>
<div id="div2" class="toolbtn"></div>
.toolbtn {
  background: url(combined_image.png);
  display: inline-block;
  height: 20px;
  width: 20px;
}
#div1 {
  background-position: -20px 0px;
}
#div2 {
  background-position: -40px 0px;
}

Create an image sprite

Combine several images into one image either by using a tool like this Sprite generator or a standard image editor. Then, use CSS to display a essentially crop the image so that only the specific part of the image displays. The sprite generator will generate the combined image and also provide the suggested CSS rules and class names.