[En pratique] Comment j’ai généré des images pour le responsive design

galerie_mini

[En pratique] Comment j’ai généré des images pour le responsive design

Bien que tout le monde s'accorde pour dire que le responsive design c'est fantastique car cela permet de facilement s'adapter à tous les écrans, toutes les tailles, on est encore loin d'avoir des solutions techniques parfaites pour le mettre en place. Le premier mirage est que pour s'adapter à tous les périphériques on a souvent une solution d'entre deux. Par exemple, pour transformer un menu horizontal en menu de sélection, on va insérer double code, et cacher via les media queries le menu qui n'est pas adapté. Résultat ? Le code source est plus lourd. Mais au niveau de l'utilisateur c'est tout de même plus pratique.

Bien sûr, quelques lignes de codes ne provoquent pas un gros goulot d'étranglement, non, c'est plutôt au niveau des images que le problème se pose : faire télécharger du 1280 pixels de large alors qu'on affiche du 50 pixels de large, cela fait une énorme différence en terme de temps de chargement, à tel point que cela influence l'expérience utilisateur (ceux qui ont essayé de consulter une galerie de photos dans le métro me comprendront).

Comment servir des images aux tailles optimales, quelles que soient les résolutions d'écrans des internautes ?

C'est exactement la problématique que j'avais sur un site, et que je souhaite partager avec vous.

Voici ma galerie de photos pour un article sur le jeu vidéo Battlefield 4 :

galerie

Mon code source est conçu pour que la largeur des images affichées dépende de la largeur de la colonne. Quand on redimensionne la fenêtre, le nombre de colonnes (5) est fixe, mais les images rapetissent de façon à s'adapter à la nouvelle largeur. La galerie affiche donc parfois les images en 280px de large, parfois en 25px, tout dépend de la résolution de l'écran. On peut dire que c'est responsive (certains me diront qu'il faudrait aussi réduire le nombre de colonnes, mais c'est un choix arbitraire de ne pas le faire).

Tout ça semble très bien, non ? Non, bien sûr que non ! Si la taille des images à l'écran varie, ce n'est pas le cas de la ressource elle-même. Peu importe que les images occupent 280px ou 25px de large, dans tous les cas le navigateur doit récupérer une ressource qui est bien plus large : une grosse image, lourde à charger, pour finalement ne l'afficher qu'en taille réduite. C'est à dire occuper de la bande passante pour pas grand chose. Autant dire que sur mobile, avec une connexion un peu limitée, la galerie met une éternité à se charger.

J'ai donc cherché des solutions pour pouvoir servir des ressources adaptées, c'est à dire des images de poids réduit, en fonction de la réalité de la résolution d'écran.

La première solution, la plus évidente je pense, est d'afficher les images en arrière plan d'un bloc, via le css, et de changer l'url de la ressource via les media queries. Ça marche. Mais c'est extrêmement lourd :

  • il faut multiplier les lignes de codes dans le css, et je ne parle pas d'une ou deux lignes : j'ai parfois des galeries avec 50 images...
  • il faut précalculer les images aux différentes tailles. C'est à dire, comme le proposent beaucoup de scripts tout faits, arbitrairement décider de 3 à 5 résolutions standards et générer les images en amont. La pré-génération ne pose pas de problème. Par contre, avoir un panel limité de résolutions fait que l'on n'est pour ainsi dire jamais sur le cas idéal. Soit je suis en dessous de la résolution requise, soit je suis au dessus. C'est de l'à peu près qui ne me semble pas robuste.

J'ai donc préféré me creuser la cervelle pour trouver une solution plus satisfaisante. En tout cas plus satisfaisante POUR MOI. En effet, c'est une solution maison, qui ne s'adaptera peut-être qu'à mon cas unique, mais qui me satisfait pleinement, vu ma configuration de serveur. Et si jamais cela peut vous donner des idées...

Place à l'action

Sur la machine, j'ai un serveur apache avec en front end un proxy nginx. Je vais utiliser un processus en plusieurs étape afin qu'une fois le système en place tout se fasse automatiquement : redimensionnement des images et affichage des ressources adaptées.

Je commence par modifier le code source de ma galerie d'images. Pour éviter de faire charger des images non adaptées (et donc occuper la bande passante inutilement), je vais afficher un gif transparent à la place des images, le tout en inline (j'évite ainsi de faire un appel extérieur inutile par image). Mais comme j'ai tout de même besoin d'avoir l'url de la ressource, je vais placer un attribut data-src indiquant l'url de l'image pleine résolution.

[cc lang="html5"]<img src="data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7" data-src="http://www.example.org/images/imageHQ.jpg">[/cc]

Ensuite, je vais utiliser une couche javascript pour qu'après le chargement de la page l'attribut src de la page soit remplacé par une url générée à la volée qui contient des indications sur l'espace occupée par le gif transparent (=la dimension réelle d'affichage dans le navigateur). Le script génère ce genre de chose :

[cc lang="html5"]<img src="http://www.example.org/images/imageHQ-size250x250.jpg" data-src="http://www.example.org/imageHQ.jpg">[/cc]

J'appelle donc une image qui n'existe pas ??!!

C'est là où est le "trick". L'url demandée va passer par le serveur nginx qui va traiter la demande de façon "astucieuse" : quand l'image n'existe pas nginx va faire passer l'info pour qu'elle soit générée. Si elle existe, nginx va la servir :

[cc lang="apache"]

location ~ ^/images/(.*)-size(d+)x(d+).jpg$ {
root /home/cache/image ;
add_header Cache-Control public;
add_header Link "<$scheme://www.example.org/images/big/$1.jpg>; rel="canonical"";
try_files /$2/$1.jpg @proxy ;
}

location @proxy{
proxy_pass http://127.0.0.1:8080;
}

[/cc]

Si le fichier image optimisé existe sur le serveur, try_files le trouve et le sert directement.
Si le fichier n'existe pas, alors c'est @proxy qui est appelé.
@proxy se contente d'envoyer la requête à travers le proxy, et donc c'est apache qui prend la relève.

Du côté d'Apache

On utilise une règle de réécriture dans le .htaccess du site :

[cc lang="apache"]

RewriteEngine On
RewriteRule ^images/(.*)-size([0-9]+)x([0-9]+).jpg$ /process.php?titre=$1&w=$2&h=$3 [L]

[/cc]

La règle de réécriture permet l'exécution de process.php avec les paramètres de taille et le nom de l'image.

[cc lang="php"]
$_GET['titre'] = preg_replace('/[^-a-zA-Z0-9_]/', '', $_GET['titre']);
$w = preg_replace('/[^-a-zA-Z0-9_]/', '', $w);

if($w>900) $w = 900; /*On ne va jamais plus loin que 900px de largeur*/
$token = md5(uniqid());
exec('mkdir '.escapeshellarg(/home/cache/image/'.$w));
$w2 = round($w*0.95); /*On crée une image légèrement plus petite que la taille optimale : la différence ne devrait pas se voir à l'écran et on réduit le poids*/
exec('cp '.escapeshellarg('/var/www/site/images/'.$_GET['titre'].'.jpg').' /tmp/'.$token.'.jpg');
exec(escapeshellcmd('mogrify -strip -resize '.$w2.'x /tmp/'.$token.'.jpg')); /*On peut optimiser davantage notamment en changeant le taux de compression*/
exec('mv /tmp/'.$token.'.jpg '.escapeshellarg(/home/cache/image/'.$w.'/'.$_GET['titre'].'.jpg'));
header("Content-Type: image/jpeg");
readfile('/home/cache/image/'.$w.'/'.$_GET['titre'].'.jpg');

[/cc]

Le script, en substance, crée un double de l'image originale aux bonnes dimensions (via mogrify qui est une application installée sur le serveur - faire le redimensionnement via php serait une mauvaise idée en terme de performances), et sert l'image à l'internaute.
Bien sûr, si on faisait l'opération à chaque appel de l'image, ce serait désastreux niveau performance. Mais grâce au stockage des variantes de l'image, le prochain appel à la même résolution d'un internaute se contente d'atteindre nginx, sans jamais atteindre apache. On y gagne.

Voilà, avec cette solution maison, je sers toujours des images aux dimensions optimales. Certes, ça prend de l'espace disque, puisqu'on peut avoir plusieurs centaines d'images pour une seule ressource, mais la bande passante de l'internaute est, elle, sauvegardée.

Mais, mon SEO ne va-t-il pas en prendre un coup ?

Pour ne pas prendre de risque au niveau du référencement web, il est prudent d'ajouter quelques subtilités :

  • toujours avoir un lien qui pointe vers la ressource HQ
  • configurer le serveur pour qu'il renvoie un header canonical pointant vers la version HQ quand on interroge les variantes de l'image HQ [edit : 20/11/2013 - Ajout du code pour le header canonical dans la configuration nginx]

Et pour aller plus loin ?

Un seul mot : lazyload.

Comments ( 8 )

  • Bonjour,
    j’ai une solution un peu similaire, sauf que le redimensionnement des images est homemade sous mono (que veux tu, on ne se refait pas :p …. )

    Par ailleurs je ne connais pas ta volumétrie, mais je te conseille d’ajouter un process qui purge les images générées de plus de 3 mois, ca fait du bien a l’espace disque…

  • GG.

    Quelques petites astuces :
    readfile à la place de ton include_once (pas d’interprétation php donc plus rapide)
    Attention au . et au / dans ton $GET[‘titre’] (possibilité de lire d’autres fichiers …)

    Pour l’histoire du gif transparent en data … je ne suis pas forcement pour. Si tu as 10 images dans ta page, tu as 10 fois le gif qui alourdit d’autant la taille de ta page.

    Un gif avec une expire loin dans le futur serait peut être préférable non ?

  • Nicolas Treguier

    moi j’utilise reponse.js pour charger conditionnellement une image ou une autre (ou un texte) en fonction de la taille de l’écran. http://responsejs.com/

  • waouw, belle solution, c’est classe.

    Pour répondre à une problématique similaire, j’avais aussi utilisé un serveur de tâche Gearman (http://gearman.org/) qui permet notamment de balancer des taches lourdes pour un serveur web à une autre machine.
    Mais là avec le serveur apache, il est possible de déporter également le traitement de l’image sur une autre bécane.

    J’aimerai bien savoir comment ton serveur renvoie la canonical de l’image originale qd on demande une image réduite, ça se fait comment ?

    Apres est ce qu’il n’est pas plus simple de n’avoir que 4 ou 5 largeurs d’images prédéfini et proposer l’affichage du format le plus proche ?

    car là quand même si on appelle
    http://www.example.org/images/imageHQ-size1x1.jpg
    http://www.example.org/images/imageHQ-size2x3.jpg
    http://www.example.org/images/imageHQ-size3x3.jpg….
    http://www.example.org/images/imageHQ-size900x900.jpg
    ça risque de faire fumer un peu les machines non ?

  • Photo du profil de Guillaume Peyronnet

    Merci pour vos commentaires 🙂

    @petitchevalroux : effectivement, il faut faire plus attention au contrôle du get ! Je ne m’étais pas trop posé la question parce que mon script initial concernait des images ayant des noms numérique (123.jpg), donc la règle apache n’avait pas de .* mais un 0-9, et le script de conversion n’est pas interrogeable en direct par le client. Mais tu fais bien de le signaler, je vais éditer le code. Merci 🙂

    Pour le gif transparent, c’est une bonne question, il faudrait comparer le gain de la suppression d’une requête http vs plusieurs insertions inline. J’imagine qu’assez vite un gif externe qui passe en cache est plus efficace.

    @Nicolas : Je ne connais pas celui là. Mais a priori on est sur des objectifs assez différent. Là mon idée était que mon script prenne tout en charge tout seul (mais j’aurais bien du mal à en faire une solution distribuée : la config est loin d’être adaptée à tous…).

    @Nicolas : Je n’ai pas de problème particulier de ressources sur la machine concernée, je n’ai donc pas pensé aux façon d’économiser les ressources. Ce qui peut être facilement fait, c’est de pré générer toutes les images pendant les périodes de creux (sur le serveur ou sur un autre serveur, pourquoi pas).
    En général les solutions d’adaptive images que j’ai croisé sur le web ciblent 3 à 5 formats. Mais je trouvais que ce n’était pas assez fin, et comme je le disais, pas de soucis de ressources m’incitant à me limiter 😉
    En réalité, on pourrait cibler assez facilement quelques formats plus répandus comme iphone/ipad/desktop et se contenter de ceux là. Mais j’aime l’idée que peu importe les futurs formats, la solution s’adaptera toujours.

    Pour le canonical, j’édite le code, ça se passe dans nginx 😉

  • Super article ! Bien expliqué et ta solution semble originale et malgré tout assez « simple » pour ne pas mettre trop le bazar.