Optimize the loading of images on a website

Web pages are increasingly heavier from year to year. To convince you, you can look at this graphic made by Tammy Everts on Soasta's blog:

As can be seen the images represent over 60% of the total weight of the page. This is because connections are getting faster and faster, so website builders can add more and more images to illustrate their pages. But this heaviness is unfortunately accompanied by several negative points including:

  • Image transfer occupies server bandwidth
  • Pages are longer to load
  • Additional costs for hosting if you pay by volume (storage and bandwidth)
  • Additional costs for the customer if we pay at the volumetric

How to remedy this?

Compress images

The first solution is to reduce the size of the images while maintaining an acceptable quality. For this it is necessary to use formats offering good compression rates and supported by different browsers. PNG or JPG formats are good candidates. Thanks to different techniques such as the reduction of the color palette of the image, it is possible to obtain better compression ratios without losing visually in quality (always check the rendering). Different web services or software exists to compress the images: tinyjpg, tinypng, jpegmini, compressor.io. If you use Grunt, Gulp or any other solution based on Node, there are also solutions: imagemin (Node), grunt-contrib-imagemin (Grunt), gulp-imagemin (Gulp).

Another solution is to use vector images whenever possible and that brings a gain. A vector image describes the image with geometric primitives (points, lines, Bézier curves, etc.). Note that a complex image may be heavier in vector than pixelated (Bitmap / Raster). SVG are well supported by browsers (source: Can I use). For example the icons of this website are SVG files.

If you want you can provide a fallback to a raster image:

<img src="logo.svg" onerror="this.removeAttribute('onerror'); this.src='logo.png'">

Provide an image at the right size

The 2nd step is to provide an image at the right size. There is no point in providing a 1000px wide image, so it will be resized to 200px via CSS. This is very easy for a fixed size image, but when you want to create a responsive site it is more complicated to send the best image. HTML5 still provides solutions including the attribute srcset allowing the browser to load the most appropriate image to the client using the screen size, and also the pixel density:

<img src="image-src.png"
     srcset="image-medium1x.png 600w 1x,
             image-medium2x.png 600w 2x,
             image-large.png 2000w">

In the case of an image in the CSS, for example background-image, it is possible to use the media queries to choose the right image:

@media (max-width: 640px) {
  .sample {
    background-image: url(image_640.png);
  }
}

@media (max-width: 1000px) {
  .sample {
    background-image: url(image_1000.png);
  }
}

You can also provide different stylesheets for each size:

<link rel="stylesheet" media="screen and (max-width: 640px)" href="screen_640.css" type="text/css" />
<link rel="stylesheet" media="screen and (max-width: 1000px)" href="screen_1000.css" type="text/css" />

Combine images (Sprites / Fonts)

CSS allows you to specify images for certain properties such as background or list-item. This allows to decorate the page with images or icons. When the number of small images specified in the CSS becomes large, it can slow down the loading time of the page (less true with HTTP/2). The solution is to combine all the small images into one and use the css background-position property to specify the area of the large image to display.

/* Muliple image */
.image1 {
    background-image: url("bg-1.png");
}
.image2 {
    background-image: url("bg-2.png");
}
/* Single image that contains multiple images*/
.image1 {
    background-image: url("sprite.png");
    background-position: 0px 0px;
}
.image2 {
    background-image: url("sprite.png");
    background-position: -16px 0px;
}

There are many services for generating sprites and associated CSS sheets from a list of images. For example: spritegen, node-sprite or grunt-spritesmith.

If you use vector images, it is possible to create Fonts. Each character of the Font will correspond to an icon. For example, this is how Font Awesome works. To create the font you can use different tools: gulp-iconfont, grunt-webfont, etc.

@font-face {
  font-family: 'MyFont';
  src: url('../fonts/fontawesome-webfont.eot');
  /* TODO add other font format (woff, woff2, ttf, svg) */
}

.icon1 {
  font: MyFont;
  content: "\f02f"; /* Unicode character of the icon in the font */
}

Load images as needed

If you examine the majority of the pages you'll realize that some images are visible as soon as the page is loaded, and that others are visible only after scrolling. This can be exploited by loading these images only when it enters the viewport.

Many libraries exist to dynamically load images on a page. Here are some examples: Lazy load XT, Unveil, lazyload. These libraries provide more or less important features. The principle is however always the same, to use an attribute other than src such as often data-src to define the url of the image. The script will transform it into src when the element will be visible to the user.

If you want to create your lazy loader, here is a good base (in TypeScript):

module LazyLoader {
    const attributeName = "data-src";

    function isElementInViewport(element: Element) {
        const rect = element.getBoundingClientRect();
        return (rect.top >= 0 &&
            rect.left >= 0 &&
            rect.bottom <= (window.innerHeight || document.documentElement.clientHeight) &&
            rect.right <= (window.innerWidth || document.documentElement.clientWidth)
        );
    }

    function loadImagesInViewport() {
        const imgs = document.querySelectorAll(`img[${attributeName}]`);
        for (let i = 0; i < imgs.length; i++) {
            const img = imgs[i] as HTMLImageElement;
            if (isElementInViewport(img)) {
                img.src = img.getAttribute(attributeName);
                img.removeAttribute(attributeName);
            }
        }

        if (imgs.length === 0) {
            unregisterEvents();
        }
    }

    var eventHandler = throttle(100, () => { // throttle: https://gist.github.com/meziantou/8807ee968f1d0b7c5464
        loadImagesInViewport();
    });

    function registerEvents() {
        document.addEventListener("DOMContentLoaded", eventHandler);
        document.addEventListener("load", eventHandler);
        document.addEventListener("scroll", eventHandler);
        document.addEventListener("resize", eventHandler);
    }

    function unregisterEvents() {
        document.removeEventListener("DOMContentLoaded", eventHandler);
        document.removeEventListener("load", eventHandler);
        document.removeEventListener("scroll", eventHandler);
        document.removeEventListener("resize", eventHandler);
    }

    registerEvents();
}

The full code is available on GitHub: https://gist.github.com/meziantou/8807ee968f1d0b7c5464. You will find in particular the code of the method throttle not presented above.

Several things can be improved. One could for example add a tolerance to load the images a little before they are visible. The srcset attribute and the picture tag are not supported. One could also add a loading image… In the case where the user deactivates the scripts, it is possible to use the tag noscript. It will however adapt the script above to take into account this case.

Conclusion

The loading time of the pages is an important factor for the user experience. Different techniques make it possible to improve it. Those presented in this article about improving the loading time of images can be useful for long pages containing a lot of images. As with any optimization, avoid doing them prematurely. Wait until you see real problems and apply the provisions to correct them.

Enjoy this blog? Buy Me A Coffee Donate with PayPal

Leave a reply