Lazy Loading d'images avec PHP et le schéma Data-URI

Comment rendre plus esthétique l'affichage des images d'une page HTML ?

5 octobre 2016

Le problème de l'affichage des images d'une page HTML

Lors de l'affichage d'une page internet via un HTTP GET, le navigateur va, dans un premier temps, afficher la page HTML puis télécharger/afficher les images.

La page HTML est affichée en considérant les images absentes puis les zones de textes sont déplacées pour laisser place aux images.

Le navigateur va essayer de télécharger le maximum d'images en parallèle selon le principe du premier arrivé, premier affiché.
Les images vont donc s'afficher de manière aléatoire. Si la page HTML est grande (dépasse les dimensions de l'écran), les images non visibles peuvent même être téléchargées avant celles qui devraient déjà être visibles.

Ci-dessous un exemple des étapes d'affichage d'une page HTML :

Problème 1

Cela engendre une latence pour l'utilisateur, une perte de bande passante et un affichage inesthétique de la page.

Les méthodes de Lazy Loading permettent de télécharger et d'afficher une image uniquement lorsque cela est nécessaire.

Pourquoi le navigateur n'effectue pas nativement du Lazy Loading d'images ?

Classiquement, une image est déclarée dans une page HTML de la manière suivante :

<img src="/your/image1.jpg" alt="test"/>

Ne connaissant pas à l'avance les caractéristiques de l'image, par exemple ses dimensions (largeur et hauteur), le navigateur ne peut pas réserver l'espace qui sera occupé par l'image, ni savoir si l'image doit être affichée/téléchargée immédiatement.

Détecter lorsque l'image est visible et réserver son espace d'affichage

La visibilité d'une image ne peut être connue que du côté client, puisqu'elle dépend du défilement de la page HTML.

La réservation de l'espace d'affichage d'une image ne peut être réalisée que du côté serveur qui stocke les images.

Visibilité de l'image

La visibilité est gérée à l'aide du JavaScript côté client, avec par exemple la librairie opensource verlok/Lazyload.

Avec ce type de solution, l'attribut src n'est pas utilisé et l'URL de l'image est placée dans un autre attribut propriétaire, par exemple data-original :

<img data-original="/your/image1.jpg" alt="test"/>

Le code JavaScript recopie le contenu de l'attribut data-original dans l'attribut src lorsque la balise <img> devient visible à l'écran,
ce qui provoque automatiquement le téléchargement et l'affichage de l'image par le navigateur.

D'après la norme HTML, l'attribut src est obligatoire et ne peut être vide.
Lui attribuer la valeur # ou #empty n'est pas apprécié par les navigateurs et les outils d'analyses, voir stackoverflow.

Deux méthodes existent pour ne pas laisser l'attribut src vide :

Réservation de l'espace d'affichage

Dans la plupart des cas les dimensions de l'image sont précisées à l'avance à l'aide des attributs width et height :

<img src="1x1.gif" data-original="/your/image1.jpg" width="400" height="300" alt="" />

Cette technique ne fonctionne pas correctement pour des pages HTML faisant appel à des styles CSS complexes ou Bootstrap.

Par exemple, en combinant les grilles avec le responsive (width="100%" et height="auto") de Bootstrap :

Problème 2

L'image à afficher est rectangulaire width="400" et height="300" mais le GIF de l'attribut src est carré, Bootstrap va réserver un espace carré jusqu'à l'affichage/téléchargement de l'image.

La solution php-lazyload-img

La librairie php-lazyload-img que nous avons développé va :

Le code HTML ci-dessous :

<!DOCTYPE html>
<html>
    <head>
    </head>
    <body>
        <div>
            <img src="img/test1.jpg"/>
        </div>
    </body>
</html>

devient automatiquement :

<!DOCTYPE html>
<html>
    <head>
    </head>
    <body>
        <div>
            <img src="data:image/gif;base64,R0lGODdhAAEAAYAAAPz+/AAAACwAAAAAAAEAAQAC/oSPqcvtD6OctNqLs968+w+G4kiW5omm6sq27gvH8kzX9o3n+s73/g8MCofEovGITCqXzKbzCY1Kp9Sq9YrNarfcrvcLDovH5LL5jE6r1+y2+w2Py+f0uv2Oz+v3/L7/DxgoOEhYaHiImKi4yNjo+AgZKTlJWWl5iZmpucnZ6fkJGio6SlpqeoqaqrrK2ur6ChsrO0tba3uLm6u7y9vr+wscLDxMXGx8jJysvMzc7PwMHS09TV1tfY2drb3N3e39DR4uPk5ebn6Onq6+zt7u/g4fLz9PX29/j5+vv8/f7/8PMKDAgQQLGjyIMKHChQwbOnwIMaLEiRQrWryIMaPGYI0cO3r8CDKkyJEkS5o8iTKlypUsW7p8CTOmzJk0a9q8iTOnzp08e/r8CTSo0KFEixo9ijSp0qVMmzp9CjWq1KlUq1q9ijWr1q1cu3r9Cjas2LFky5o9izat2rVs2x4rAAA7" 
                 data-original="img/test1.jpg"
                 width="256" height="256" />
        </div>
    </body>
</html>

Ci-dessous les étapes d'affichages avec un Data-URI de couleur unie :

Solution unie

Ci-dessous les étapes d'affichages avec un Data-URI miniature :

Solution avec perte

Comment utiliser php-lazyload-img ?

Côté serveur

Créer le fichier package.json contenant :

{
    "require": {
        "glicer/php-lazyload-img": "dev-master"
    }
}

Installer la librairie à l'aide de composer :

composer install

Exemple de code PHP :

<?php
require 'vendor/autoload.php';

use GlLazyLoadImg\GlLazyLoadImg;
use Symfony\Component\Finder\Finder;

$finder = new Finder();
$files  = $finder->files()->in(__DIR__ . "/html/")->like('*.html'); //récupérer la liste de tous les fichiers html

/*
 __DIR__ est utilisé pour résoudre le chemin d'accès absolu aux images
 GlLazyLoadImg::BLANK pour générer un Data-URI avec une image unie de taille proportionnelle à l'originale
     sinon GlLazyLoadImg::LOSSY pour générer une miniature de l'image originale

 le contenu de l'attribut `src` sera déplacé dans l'attribut `data-original`
*/

$lazyload = new GlLazyLoadImg(__DIR__, GlLazyLoadImg::BLANK, 'data-original'); 

foreach ($files as $file) {
    $html = file_get_contents($file->getPath());        //lire le contenu du fichier html
    $result = $lazyload->autoDataURI($html);            //ajouter le data:uri, width, height, data-original au contenu html
    file_put_contents($result, $file->getPath());       //écraser le fichier html
}

Côté client

Exemple du fichier HTML avec la librairie JavaScript verlok/LazyLoad :

<html>
    <head>
    </head>
    <body>
        <script src="js/lazyload.min.js"></script>
        <script>
            var myLazyLoad = new LazyLoad();
        </script>
    </body>
</html>