Les pages web sont de plus en plus lourdes d’année en année. Pour vous en convaincre vous pouvez regarder ce graphique réalisé par Tammy Everts sur le blog de Soasta.

Comme on peut le voir les images représentent un peu plus de 60% du poids total de la page. Cela s’explique par le fait que les connexions sont de plus en plus rapide, et donc que les créateurs de site web se permettent d’ajouter de plus en plus d’images pour illustrer leurs pages. Mais cette lourdeur s’accompagne malheureusement de plusieurs points négatifs dont notamment :

  • Le transfert des images occupe la bande passante du serveur
  • Les pages sont plus longues à charger
  • Coûts additionnels pour l’hébergement si on paye à la volumétrie (stockage et bandwidth)
  • Coûts additionnels pour le client si on paye à la volumétrie

Comment remédier à cela ?

Compresser les images

La première solution est de réduire la taille des images tout en conservant une qualité acceptable. Pour cela il faut utiliser des formats offrant des bons taux de compression et pris en charge par les différents navigateurs. Les formats PNG ou JPG sont des bons candidats. Grâce à différentes techniques telles que la réduction de la palette de couleur de l’image, il est possible d’obtenir de meilleurs taux de compression sans perdre visuellement en qualité (pensez à toujours bien vérifier le rendu). Différents services web ou logiciels existent pour compresser les images : tinyjpgtinypngjpegminicompressor.io. Si vous utilisez Grunt, Gulp ou toute autre solution basée sur Node il existe également des solutions : imagemin (Node), grunt-contrib-imagemin (Grunt), gulp-imagemin (Gulp).

Dans l’exemple ci-dessous, la deuxième image est presque 3 fois plus légère :

image lourde

image légère

Une autre solution est d’utiliser des images vectorielles lorsque cela est possible et que cela apporte un gain. Une image vectorielle décrit l’image avec des primitives géométriques (points, traits, courbes de Bézier, etc. ). On comprend donc qu’une image complexe sera peut-être plus lourde en vectorielle qu’en pixellisée (Bitmap/Raster). Les navigateurs prennent en charge pour la grande majorité le format SVG (source : Can I use). Par exemple le logo de SoftFluent (en haut à gauche de cette page) est un SVG, avec en cas d’erreur un fallback vers l’image en PNG.

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

Fournir une image à la bonne taille

La 2ème étape est de fournir une image à la bonne taille. Rien ne sert de fournir une image de 1000px de largeur alors quelle sera redimensionnée à 200px via le CSS. Cela est très facile pour une image de taille fixe, mais lorsque l’on souhaite créer un site responsive il est plus compliqué d’envoyer la meilleure image. HTML5 apporte tout de même des solutions notamment avec l’attribut srcsetpermettant au navigateur de charger l’image la plus adaptée au client en utilisant la taille de l’écran, et également la densité de pixels :

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

Dans le cas d’une image dans le CSS, par exemple background-image, il est possible d’utiliser les media queries pour choisir la bonne image :

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

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

On peut également fournir des feuilles de styles différentes :

<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" />

Combiner les images (Sprites / Fonts)

Le CSS permet de spécifier des images pour certaines propriétés telles que background ou list-item. Cela permet d’agrémenter la page avec des images ou des icônes. Lorsque le nombre de petites images spécifiées dans le CSS devient important, cela peut ralentir le temps de chargement de la page (moins vrai avec HTTP/2). La solution est de combiner toutes les petites images en une seule et d’utiliser la propriété css background-position pour spécifier la zone de la grande image à afficher.

.image1 {
    background-image: url("bg-1.png");
}
.image2 {
    background-image: url("bg-2.png");
}
.image1 {
    background-image: url("sprite.png");
    background-position: 0px 0px;
}
.image2 {
    background-image: url("sprite.png");
    background-position: -16px 0px;
}

Il existe de nombreux services permettant de générer des sprites et les feuilles CSS associées à partir d’une liste d’images. Par exemple : spritegennode-sprite ou grunt-spritesmith.

Si vos images sont toutes vectorielles il est possible de créer des Fonts. Chaque caractère de la Font correspondra à une icône. C’est par exemple comme cela que fonctionne Font Awesome. Pour créer la font vous pouvez utiliser différents outils : gulp-iconfontgrunt-webfont, etc.

@font-face {
  font-family: 'MyFont';
  src: url('../fonts/fontawesome-webfont.eot'); 
  /* TODO ajouter les autres formats de font (woff, woff2, ttf, svg) */
}

.icon1 {
  font: MyFont;
  content: "\f02f"; /* caractère unicode correspondant à l'icone */
}

Charger les images quand nécessaire

Si on examine la majorité des pages on se rend compte que quelques images sont visibles dès le chargement de la page, et que d’autres sont visibles seulement après avoir commencé à scroller. On peut exploiter cela en chargeant ces images seulement lorsqu’elle entre dans la zone visible par l’utilisateur (viewport).

De nombreuses bibliothèques existent pour charger dynamiquement les images d’une page. Voici quelques exemples : Lazy load XTUnveillazyload. Ces bibliothèques fournissent des fonctionnalités plus ou moins importantes. Le principe est cependant toujours le même, utiliser un attribut autre que src tel que souvent data-src pour définir l’url de l’image. Le script le transformera en src lorsque l’élément sera visible pour l’utilisateur.

Si vous souhaitez créer votre lazy loader, voici une bonne base (en 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();
}

Le code complet est disponible sur GitHub : https://gist.github.com/meziantou/8807ee968f1d0b7c5464. Vous y trouverez notamment le code de la méthode throttle non présenté ci-dessus.

Plusieurs choses peuvent être améliorées. On pourrait par exemple ajouter une tolérance afin de charger les images un peu avant qu’elles ne soient visibles. L’attribut srcset et la balise picture ne sont pas supportés. On pourrait également ajouter une image de chargement… Dans le cas où l’utilisateur désactive les scripts, il est possible d’utiliser la balise noscript. Il faudra cependant adapter le script ci-dessus pour prendre en compte ce cas.

Conclusion

Le temps de chargement des pages est un facteur important pour l’expérience utilisateur. Différentes techniques permettent de l’améliorer. Celles présentées dans cet article concernant l’amélioration du temps de chargement des images, peuvent s’avérer utile pour des longues pages contenant beaucoup d’images. Comme pour toute optimisation, évitez de les faire de manière prématurée. Attendez de constater de vrais problèmes et appliquez les dispositions pour les corriger.

Ne ratez plus aucunes actualités avec la newsletter mensuelle de SoftFluent