Mise à jour : saison printemps – été

Par

Base64 et canvas : avant/après

Partager 👉

Cet article vous plaît ? N'hésitez pas à le partager 👉

Cela fait fichtrement longtemps que je n’ai pas rédigé un article de devblog dont tout le monde se moque, et pourtant… Pourtant, ce blog a bien évolué depuis la dernière fois !

Initialement, cette mise à jour devait se concentrer sur un point simple : la modification du design de l’en-tête des articles. Comme vous allez le voir ci-dessous, ça a légèrement dérapé. Voici la liste des p’tits trucs chouettes qui sont apparus pour la mise à jour d’hier et celle d’il y a quelques mois (je vous laisse grappiller ce qui vous intéresse).

Graphisme / UX :

  • Les en-têtes d’articles ont été modifiées et un nouveau module de partage s’affiche en fin de contenu (accès direct);
  • Les animations de transition entre les pages sont toutes neuves (accès direct);

Développement (attention : je me suis laissé emporter, c’est assez technique) :

  • Les images chargent avec un fond flouté (accès direct) ;
  • Les pages sont globalement mieux optimisées (elles chargent plus vite et s’affichent sans tuer votre ordinateur) (accès direct);

Et pour connaître la suite du programme, c’est par ici..

Modification des articles et module de partage

L’en-tête précédente des articles m’avait permis de jouer graphiquement avec les traits et la grille générale du site, mais ne me satisfaisait pas, autant dans son design que son contenu. L’idée de cette mise à jour est de proposer davantage d’informations utiles en accès rapide tout en ouvrant la voie vers le prochain chantier concernant les auteurs. Je me suis fortement inspiré de la présentation de pochettes d’album, notamment sur YouTube.

J’ai essayé d’équilibrer le design en contrebalançant la masse de l’image principale par le contenu textuel, avec un bon sens de lecture gauche – droite. Les différences de taille de typographie encouragent l’œil à se poser d’abord sur le titre, avant de glisser sur le trait vers l’auteur. Les catégories et étiquettes harmonisent le tout et offrent dans un troisième temps la possibilité au visiteur de naviguer vers des thèmes similaires. Enfin, j’ai veillé à ce que l’effet de flou et de baisse de luminosité de l’image en fond soient suffisants pour préserver une bonne lisibilité. Si ce n’est pas le cas pour vous, mes plus plates excuses, je vous laisse m’en faire part dans les commentaires.

Et bonus : si la progression et les étapes de recherche vous intéressent, j’ai tweeté à ce sujet.

Quant aux partages… Bon. Là, c’est un peu gênant, parce que la précédente version du système de partage d’articles était tout simplement bugguée presque depuis sa sortie (?). C’est resté complètement inutilisable pendant quoi…? 7 ou 8 mois ? Sombre histoire de conflits entre des z-index.

Qu’importe ma honte, il était grand temps de s’occuper de ce module. Le paradoxe à traiter est le suivant : pour que les gens partagent, il faut leur forcer la main mais, d’un autre côté, je déteste les forçages de poignet agressifs. J’ai donc essayé de trouver un compromis peu intrusif, qui sera de toute manière plus utile que sa contrepartie précédente, inexistante sous mobile et plantée sur ordinateur. Et donc : un simple modal arrivant en bas de l’écran propose les réseaux sociaux principaux lorsque le lecteur arrive à la fin de l’article (je me documente sur la possibilité d’ajouter un moyen de copier le lien, mais cela semble mal supporté par les navigateurs).

D’après les statistiques associés à l’engament sur le web (les gens ne lisent que les X premiers pourcents de la page et, au mieux, survolent le reste ; je plaide coupable), ce système sera très marginalement utilisé. Néanmoins, je me dis que les personnes le voyant seront les plus enclines à partager l’article, et auront pu profiter d’une lecture agréable, sans pop-up intrusif. Il faut savoir chérir ses lecteurs ; ils le rendent généralement bien.

Changement des transitions

Un des objectifs de ces dernières mises à jour est d’offrir une expérience utilisateur agréable, plus spécifiquement lors de la navigation. Cela passe par une réduction maximale du temps de chargement initial, mais aussi par un travail sur les animations entre les pages.

Auparavant, le comportement du site lors d’un clic sur un lien n’était pas spécialement prévisible. Un coup, l’image s’agrandissait pour devenir la miniature, un autre des panneaux latéraux semblables à ceux du menu sur ordinateur faisaient disparaître la vue précédente… Bref, il y avait un réel besoin d’uniformisation pour que le changement de contexte ait une sensation similaire tout au long de navigation, pour que l’utilisateur ne soit ni surpris ni perdu à chaque transition.

De même, j’avais initialement utilisé une chouette fonctionnalité d’AnimeJS, permettant de dessiner le pourtour d’un SVG, et donc celui du logo du blog. L’animation était sympathique, mais durait une seconde et demi. À chaque chargement du site. C’est effroyablement long, à tel point que je la désactivais en phase de développement, et je plains sérieusement tous ceux qui ont eu le temps de faire des courses, de boire du thé et d’apprendre une nouvelle langue pendant que cela se finissait. La perception du temps de chargement réel était fichtrement faussée, puisque le serveur finit de répondre bien avant que l’animation ne se termine et je mourrais à l’intérieur à chaque fois que je vous imaginais vieillards ou partis depuis longtemps lorsque le contenu apparaissait enfin. Bref, j’en ai profité pour modifier cette partie en même temps que les transitions entre les pages.

Placeholder Et hop la transition !
Et hop la transition !

D’un point de vue du graphisme, j’ai essayé de garder ça plutôt épuré, avec un point de départ similaire : un panneau partant du bas et cachant la page, puis ce même élément se retirant vers le haut. Entre temps, du texte, une image ou les deux, apparaissent et glissent légèrement vers le haut. J’aime les animations plus ou moins organiques, élastiques, et celle-ci ne fait pas exception. J’ai essayé de réduire au maximum son temps d’exécution, pour garder une bonne fluidité dans la navigation tout en évitant les saccades. C’est d’ailleurs pour ça que sur petits écrans, elle dure environ 100ms de plus, pour laisser un peu de temps aux téléphones de calculer les différentes animations.

Pour ce faire, j’utilise toujours AnimeJS et BarbaJS, dans sa version 1 et non 2, car plus légère (4.4Kb minifiée, héhé) avec moult Promise. Ce changement m’a au passage permi de bien factoriser le code, en utilisant par exemple des classes pour déterminer quels éléments vont glisser vers le haut lors de la transition. Cela fait plutôt bien son travail ; si vous avez des questions à ce sujet, je suis disponible, mais ce n’est pas spécialement intéressant à détailler ici.

Chargement des images

TL;DR :

  • Affichage / animations moins gourmandes en terme de ressources ;
  • Utilisation de src-set pour charger l’image de taille la plus proche de celle d’affichage ;
  • Optimisation du nombre de requêtes ;
  • Le tout fait main, sans importer de librairie pour rien.

Toutes les images en-dessous de la ligne de flottaison sont chargées à la flemmarde. La précédente version du lazy load utilisait la librairie Color Thief pour récupérer la couleur moyenne de l’image lors de son clic dans le backend de WordPress. Il fallait donc que l’auteur clique sur l’illustration pour que le script se lance avec des sélecteurs de DOM foireux et que les codes hexadécimaux soient ajoutés au custom field. Bon. Ça marchait, mais à la moindre modification du markup de l’interface d’administration, ça allait foirer, et j’ai mis 2h à mettre à jour toutes les images existantes avec un script fait maison.

Peu serein face à l’aspect bancal de cette solution et devant le manque d’optimisation de l’animation associée, je me suis penché sur le problème. La solution retenue s’approche de celle rencontrée sur Medium : un placeholder flouté est utilisé le temps que l’image finale soit chargée.

En s’intéressant davantage à ce qu’il y a sous le capot :
Lors de l’upload, une version de 6 pixels par 6 pixels au maximum (on sauvegarde le ratio initial, n’exagérons rien) est créée grâce à Imagick. On la sauvegarde, encodée en base64, dans les metadatas associées à l’image. Ainsi, on peut aisément récupérer cette chaîne de caractères pour l’utiliser côté client et le processus est transparent, robuste et aisément automatisable grâce à la régénération des tailles d’images de WordPress. Yay ! (Ce code est disponible dans functions.php, si cela vous intéresse.)

Retour chez le client : on récupère la base64 que l’on place dans une balise img, avec un bon display: none; des familles. A côté d’elle, on retrouve deux balises tout aussi sympathiques : un canvas dont les attributs de largeur et hauteur ont été renseignés en fonction du ratio de l’image initiale (j’ai dû utiliser une variable globale en PHP, pardonnez-moi svp) et une autre image, contenant dans <code<data-src l’ensemble des valeurs pour son src-set.

Au chargement de la page, on ajoute aux canvas l’image encodée en base64 associée. Comme l’image dans le canvas est étirée, l’effet flouté se crée automatiquement; pas besoin de bidouiller avec un filtre CSS (externe ou intégré au canvas).

Placeholder Base64 et canvas : avant/après
Base64 et canvas : avant/après lazy load

Ensuite, grâce à un intersectionObserver (et à son polyfill), on attend que l’utilisateur scroll devant l’image et là, hop, on récupère les valeurs de src-set pour que le navigateur fasse la bonne requête selon la taille de la fenêtre en les ajoutant à l’image. Grâce au principe d’intrisic padding, l’image ajoutée se place correctement au-dessus du canvas et on la fait apparaître avec une petite animation sur l’opacité (optimisation CPU). L’utilisation du padding permet d’éviter tout content reflow non désiré à l’ajout de l’image dans la page, qui détériore les performances.

Initialement, je n’utilisais pas le canvas et m’amusais à mettre la version en base64 étirée dans une balise img et floutée grâce à filter: blur() en CSS, mais cette itération, bien que plutôt jolie, n’était tout simplement pas viable en terme de performances. La navigation sur ordinateur n’était pas glorieuse, et celle sur mobile était ressemblait à une séance de diapositives dans une cave mal éclairée. Aujourd’hui, d’après mes différents tests et les retours recueillis, ça a l’air de fonctionner plutôt correctement.

J’ai fait le choix de ne pas utiliser d’image avec une résolution plus importante, comme le fait Medium. Leur technique utilise une version d’environ 50 pixels avant de charger l’originale et je souhaite réduire au maximum le nombre de requêtes nécessaires, pour réduire le temps de chargement général. Utiliser une version en base64 permet un chouette effet finalement assez proche (les images chargent relativement vite, si le serveur n’est pas en rade), tout en étant extrêmement léger. C’est aussi pour ça que je me suis limité à 6 pixels par 6, histoire de ne pas avoir des chaînes de caractères trop importantes à transférer dans l’HTML.

Optimisations diverses

Réduction du temps de chargement

TL;DR : passage de 2100 à 460kB pour la page d’accueil ; 99/100 à 100/100 sur PSI.

Depuis que j’ai découvert PageSpeed Insights, seul le nombre vert à 3 chiffre m’intéresse. Ce n’est pas encore le cas pour le mobile, mais depuis cette mise à jour, l’artboratoire atteint les 99 et 100 respectivement sur mobile et ordinateur, soit une évolution de 6 et 2 points. Ce n’est pas grand chose et il reste quelques points à améliorer, mais c’est mon péché mignon et je m’éclate à optimiser tout ça.

Dès le commencement du thème, un choix assumé a été de produire un maximum de fonctionnalités seul, en réduisant à l’extrême les librairies tierces et en préconisant des solutions légères. C’est d’ailleurs en partie pour cela que j’utilise AnimeJS et non GSAP. Cela dit, quelques dizaines de kilo octets représentent peu face au poids des images ou des polices. C’est pourquoi une des premières démarches entreprises pour cette volée d’optimisations a été de redimensionner les illustrations au mieux et d’ajouter une version Webp avec redirection automatique pour chaque image du site. Globalement, le gain de poids est de… beaucoup trop ? Je n’ai plus les chiffres exacts en tête, mais c’est impressionnant.

Dans la même veine, j’ai enfin pris le temps d’utiliser autre chose que du .ttf en profitant pleinement de @font-face et des nouveaux formats de polices. Quelques conversions et 8 lignes de CSS plus tard, le navigateurs supportant le .woff2 sont passés de 518 à… 165kB au total. Ça fait du bien.

Par ailleurs, j’en ai profité pour revoir la manière dont je chargeais les scripts.
Précédemment, une page chargeait 16.68+12.83+20.24+6.66+38.65 = 95.06kB (31.47kB gziped), avec 5 requêtes dont certaines bloquantes. Aujourd’hui, elle ne charge plus que 89.64kB (27.62kB gziped) d’un seul coup en lazy load, ce qui est une amélioration minime, mais dont je suis plutôt fier. Cela permet d’utiliser au mieux gzip comme le volume de texte est plus important et charger le script à la flemmarde offre un affichage de la page plus rapide.

Ceci s’est accompagné d’une réduction globale du chemin critique : le CSS minimal est ajouté inline dans le header pour cacher la page et éviter tout FOUC (Flash of Unstyled Content), avant que le reste ne soit chargé en asynchrone, avec en bonus un rel=preload pour les navigateurs le supportant (cf >critical-path.php) . Du côté du JavaScript, un simple defer suffit, ce qui l’extrait du chemin critique ; il ne reste donc plus que les 2 polices d’écriture que je n’arrive pour l’instant pas à exclure.

En réalité, je me pose des questions sur l’utilité de la démarche pour le bundle JS, le serveur utilisant HTTP2 et pouvant donc traiter plusieurs requêtes en parallèle, réduisant fortement l’intérêt de regrouper les ressources de la sorte. De la même manière, il faut que je fasse des tests par rapport au Max Potential First Input Delay : utiliser plusieurs fichiers de scripts libère le thread principal du navigateur plus régulièrement au moment critique du chargement. Mais d’un autre côté, j’utilise désormais gzip à son potentiel maximum donc à voir. Dites-moi ce que vous en pensez dans les commentaires / par mail / sur Twitter.

[EDIT du 19 juillet 2019 : effectivement, après différents tests, il est plus rentable pour moi de séparer en deux fichiers ce que je traitais précédemment comme un gros paquet. Chiffonné par le petit nombre orangé disant que le JavaScript occupait trop longtemps le thread principal du navigateur, j’ai simplement mis d’un côté les librairies, de l’autre le code personnel. Désormais, tout est au vert !]

Enfin, le bundle JavaScript a vu sa taille réduite car j’en ai secoué les branches avec force et détermination. Le principe du tree shaking repose sur l’import des modules en ES6, qui, peuvent ajouter la totalité du code ou seulement celui de la fonction concernée. En gros, j’ai remplacé mes import * par de jolis import {fonctionCiblee}. Et hop, moins de kB à charger !

Placeholder Performances : 100/100 !
Et hop ! Tout le monde s’en moque, mais c’est classe

Il reste encore quelques pistes intéressantes pour réduire le bundle, notamment remplacer HammerJS par une implémentation maison des 2 fonctions et demi que j’utilise, mais c’est le genre de chantier auquel je m’intéresserai dans plusieurs mois, d’après le gain réel que cela apporte. [EDIT du 19 juillet 2019 : c’est fait, avec une chouette implémentation à base d’objets étou ! Cf. ce fichier si cela vous intéresse.]

Optimisation du JavaScript

Au-delà de la taille de paquet et de son chargement, je me suis intéressé au code JavaScript lui-même. Ici, rien de bien glorieux ou incroyable, mais j’ai profité du changement des transitions pour factoriser une bonne partie des fonctions.

De même, j’utilise désormais un objet global contenant les mesures de la fenêtre, qui me servent ensuite pour quelques tests pour l’animation du menu ou les animations, par exemple. Cette solution permet de limiter le layout reflow (cf cette chouette liste) au maximum et d’éviter de surcharger la stack d’appels inutiles. Je ne suis pas certain que la solution adoptée soit parfaite ; elle se présente néanmoins comme un meilleur parti que la précédente.

Enfin, j’ai spam CTRL+MAJ+F pour remplacer les forEach par des for of et les querySelector* par des getElementsByClassName dès que possible (j’en ai profité pour casser le menu en passant en prod, bien joué moi). Sur le papier, ces deux alternatives sont bien plus performantes mais je doute que vous soyez nombreux à noter une réelle différence.

A venir

Dans les prochains mois, je vais essayer de travailler sur les points techniques suivants :

  • Continuer à traquer les bugs divers et variés (laissez un commentaire si un comportement du site vous semble suspect) ;
  • L’accessibilité générale : il y a plein de choses à mettre en place, dont des très simples, et j’aimerais pouvoir améliorer l’expérience de navigation pour un maximum de personnes ;
  • De plus jolies pages pour les auteurs : ils participent au blog et la moindre des choses est de leur offrir une jolie vitrine pour se présenter ;
  • Continuer à optimiser (il reste tant de détails à régler et c’est si amusant, je n’arrive pas à m’en empêcher).

Merci d’avoir lu jusqu’ici, j’espère que cela vous a intéressé. N’hésitez pas venir papoter de design, dev web ou autre, ainsi qu’à aller lire les autres articles aux alentours, qui seront bien plus digestes et carrément plus artistiques que celui-ci !
À une prochaine fois.

PS : merci à Julie, Marnie, Nicolas, Guillaume, David, Hyacinth, I. et M. pour leur patience et leurs conseils avisés.


Accès direct aux commentaires

L'article vous plaît ?

Vous avez sûrement un avis à partager !

* : requis.