Cocktailand - Gérer ses images sous symfony

Comment faire pour afficher plein d'images sur une page web sans trop impacter les performances du site

Publié le 05/06/2018

La page d'accueil de cocktailand affiche en moyenne 35 images. Dans sa première version je n'avais pas pris la peine de soigner la gestion des images. J'avais pour objectif de sortir le produit minimum viable, je me suis donc attardée sur les fonctionnalités principales:

  • L'ajout des recettes de cocktail
  • L'affichage des recettes de cocktails
  • La catégorisation
  • Le moteur de recherche

Cette méthodologie permet de sortir rapidement un site de base et de l'enrichir de façon successive.

Le fait d'avoir mis en place varnish et les esis faisait que la page s'affichait très rapidement mais les images mettaient du temps à s'afficher.

Dans cet article je vais faire le point sur les différentes techniques à mettre en place pour réduire au maximum le temps de chargement des images sur votre site WEB.

Optimiser les images

La première chose à faire est de redimensionner votre image à une taille raisonnable.

Il est rare d'avoir besoin d'une image avec une résolution plus grande que 1024x768 pixels.

La seconde étape est d'optimiser vos images à l'aides d'outils comme imageoptim, compressor.io ou tout autre outil du genre.

Vous pouvez régler l'outil pour avoir ou non de la perte de qualité au profit d'un gain de taille.

Il n'est pas rare de gagner 70% de taille sur une image.

Compression d'image avec imageoptim

Sur 35 images de 1Mo on gagnerait 25Mo.

Cache http

Maintenant que nous avons optimisée la taille de nos images au maximum et que le visiteur les a téléchargées, nous allons dire à son navigateur de les conserver pour la prochaine fois.

Côté varnish j'ai pour habitude d'avoir cette règle qui surcharge le retour de nginx pour ajouter une durée de cache de 24.

sub vcl_backend_response {
      if (bereq.url ~ "\.(jpe?g|png|gif|pdf|tiff?|css|js|ttf|woff2?|otf|eot|svg)$") {
          set beresp.ttl = std.duration(beresp.http.age+"s",0s) + 24h;
      }
}

Cette règle est géniale si chaque fichier est immuable et a une url unique. Cela implique lors de votre build ajoute un hash unique dans le nom de vos JS et CSS. Si vous avez des fichiers qui changent au cours de la journée cette solution n'est pas faite pour vous.

Http 2

Http 2 permet de télécharger d'optimiser très largement le temps de téléchargement des assets, car il conserve la même connexion au serveur. HTTP 1 pour sa part initialise une connexion par fichier à télécharger. Cette démonstration http2 vs http1 va vous permettre de vous rendre compte du gain par rapport à http 1.

Http 1 vs Http 2 performance Le site va télécharger une mosaïque qui est composée de 100 petites images en http 1 puis en http 2. Le gains est de environ 60% de temps de chargement sur mon macbook pro et une connexion fibre.

Si vous ne savez pas comment faire, ou que vous n'êtes pas techniquement sûr de comment gérer http2 sur votre serveur vous pouvez utiliser cloudflare. Par défaut cloudflare va gérer les connexions http 2 tout en continuant de faire des appels http 1 sur votre serveur. Il vca s'occuper de multiplexer les requêtes et tout se passera de manière transparente pour vous. Ce n'est pas parfait mais c'est mieux que rien...

Prés-calculer les différentes tailles

Pour la partie serveur nous avons maintenant une stack idéale pour afficher rapidement nos images.

Il ne reste que quelques détails à régler. Dans la première partie je vous ai dit de redimensionner vos images au format maximum que vous pourrez un jour utiliser.

Mais pour avoir de bonnes performances il va falloir aller plus loin.

C'est le moment de prés-calculer l'ensemble des tailles d'images que vous allez utiliser. L'idée est de ne faire télécharger que le strict nécessaire à l'utilisateur. Si nous avons besoin d'une image de 64x64 pixel il est préférable d'envoyer une image qui fait déjà la bonne taille plutôt que de télécharger une grosse image et de laisser le navigateur la redimentionner. Vous allez gagner en bande passante mais aussi en CPU.

Par exemple sur cocktailand une image peut actuellement être utilisée avec les tailles suivantes:

  • 64x64
  • 240x180
  • 270x200
  • 400x400

Pour un peu plus de 1000 images sur le site le travail n'est pas faisable à la main. D'autant plus que je ne m'interdis pas d'ajouter de nouvelles tailles si je modifie le design ou les supports.

Sur Symfony il existe un bundle liip/imagine-bundle qui permet cette automatisation. Le bundle va générer à la volé les miniatures lors de la première demande puis les stocker sur le serveur pour les prochaines demandes.

Une fois le bundle installé dans votre projet vous n'avez qu'à enregistrer vos formats dans la configuration et à utiliser le filtre twig.

liip_imagine:
    resolvers:
       default:
          web_path:
              web_root: "%kernel.project_dir%/public"
              # en écrivant ici on garde le cache entre chaque release
              cache_prefix: "images/cache"

    loaders:
        default:
            filesystem:
                data_root: "%kernel.project_dir%/public/"

    cache: default
    data_loader: default

    # valid drivers options include "gd" or "gmagick" or "imagick"
    driver: "gd"

    # define your filter sets under this option
    filter_sets:

        # an example thumbnail transformation definition
        # https://symfony.com/doc/current/bundles/LiipImagineBundle/basic-usage.html#create-thumbnails
        thumbnail:
            jpeg_quality: 75
            png_compression_level: 8
            filters:
                auto_rotate: ~
                strip: ~
                thumbnail:
                    size:          [64, 64]
                    mode:          inset
                    allow_upscale: true

        large:
            jpeg_quality : 90
            png_compression_level: 6
            filters:
                auto_rotate: ~
                strip: ~
                thumbnail:
                    size:          [400, 400]
                    mode:          inset
                    allow_upscale: true

        medium_desktop:
            jpeg_quality : 80
            png_compression_level: 6
            filters:
                auto_rotate: ~
                strip: ~
                thumbnail:
                    size:          [240, 180]
                    mode:          inset
                    allow_upscale: true

        ## ...

On pourra remarquer que plus l'image est petite plus j'ai fait baisser la qualité.

Résultat pour une image déclinée dans l'ensemble des tailles:

Mojito alsacien miniature Mojito alsacien taille 2 Mojito alsacien taille 3 Mojito alsacien taille 4

Respectivements les tailles sont de 2,1Ko, 8,6Ko, 11,4Ko, 41,9Ko. Il y a donc un facteur 20 entre la miniature et le plus grand format utilisé sur le site.

Lazy loading / image set

Le dernier point permet un énorme gain de performance, mais il peut aussi impacter votre SEO si vous le faites mal. L'idée est de ne faire télécharger au visiteur que les images qui seront immédiatement visible à l'écran. Si l'image n'est pas visible à l'écran elle ne sera pas téléchargée au chargement, mais au scroll quand elle entrera dans le viewport. Le problème réside dans le fait de mettre en place cette technique et en laissant google (ou autre) voir les images. Le paquet npm vanilla-lazyload est vraimeny sympa car il répond aux deux points précédent et en plus il permet de mettre en place des "images responsives" via les srcset.

Une fois le paquet installé vous n'avez quasiment rien à faire pour initialiser la librairie

const LazyLoad = require('vanilla-lazyload');
let lazyLoad = new LazyLoad();

Une fois la librairie mise en place et instanciée côté javascript il ne reste plus qu'à utiliser les data attributes des images pour l'utiliser. Voici un exemple d'utilisation avec imagine.

<img alt="Recette simple du Mojito alsacien"
        data-src="{{ asset(cocktail.image)|imagine_filter('medium_desktop') }}"
        data-srcset="{{ asset(cocktail.image)|imagine_filter('medium_mobile') }} 200w, {{ asset(cocktail.image)|imagine_filter('medium_tablet') }} 400w"
        sizes="(min-width: 20em) 35vw, 100vw">

Conclusion

Une fois l'ensemble des techniques misent en place le chargement de la home ne télécharge "que" 1.2 Mo d'image, publicités incluses. Voici des données issues de webPageTest concernant la home.

Le premier chargement de la page pour un visiteur:

Sans cache

Le second chargement de la page pour un visiteur:

Avec cache

On remarque que le nombre d'assets téléchargés lors du second chargement est plus faible. Si on regarde en détail la liste des fichiers téléchargés on constate, avec chagrin, que plus rien ne provient de cocktailand. Il ne reste en effet que le tracking et la publicité.

Liste des fichiers