Un logo affiché à 15 pixels semblait un peu plus épais dans Chrome que dans Firefox.
Guillaume Técher a d’abord fait ce que ferait n’importe quel développeur raisonnable : remplacer le JPEG par un SVG. Le problème visuel a disparu.1
Puis il a fait quelque chose de moins raisonnable mais beaucoup plus intéressant : chercher pourquoi deux navigateurs pouvaient donner un résultat différent à partir du même petit JPEG.
La piste mène jusque dans le décodage de l’image. Quand Chrome doit afficher un JPEG très loin de sa taille d’origine, Skia peut demander à libjpeg-turbo de décoder directement une version réduite, au lieu de reconstruire tous les pixels puis de réduire le bitmap ensuite.12
À très petite taille, cette optimisation peut devenir visible.
Un JPEG est déjà rangé par fréquences
JPEG découpe notamment l’image en blocs et représente leur contenu avec une transformation en cosinus discrète, ou DCT. Chaque bloc contient alors des coefficients correspondant grossièrement à des variations plus lentes ou plus rapides dans l’image.
Une zone presque uniforme vit surtout dans les basses fréquences. Les détails fins, les bords et les textures demandent davantage de hautes fréquences.1
Quand une image de plusieurs centaines ou milliers de pixels est affichée en quelques dizaines de pixels, une grande partie de ce détail fin disparaîtra de toute façon.
Il existe donc une optimisation élégante : ne pas reconstruire à pleine résolution des informations qui seront immédiatement jetées pendant la réduction.
libjpeg-turbo expose précisément des extensions de mise à l’échelle pendant l’IDCT. Sa documentation liste des facteurs comme 1/8, 1/4, 3/8, 1/2, 5/8, 3/4 et 7/8 pour le décodage.3
Skia cherche une réduction que le décodeur sait faire
Le code de SkJpegCodec montre directement le mécanisme. Pour vérifier si une dimension demandée peut être obtenue nativement, Skia essaie les fractions 8/8, 7/8, 6/8 et ainsi de suite jusqu’à 1/8, puis configure scale_num et scale_denom dans libjpeg.2
Cela signifie que le pipeline n’est pas toujours :
- décoder le JPEG entier ;
- produire un gros bitmap ;
- réduire ce bitmap jusqu’à 15 pixels.
Il peut commencer par un JPEG déjà décodé à une fraction de sa taille, puis finir l’ajustement avec l’algorithme de mise à l’échelle du moteur graphique.12
Pour une photo, c’est généralement exactement ce qu’on veut : moins de mémoire et moins de travail pour des détails qui ne survivront pas à l’affichage.
Pour une icône minuscule avec des bords très précis, le compromis devient plus facile à voir.
À 1/8, certains détails n’entrent plus dans le voyage
Dans l’exemple de Técher, le JPEG était suffisamment réduit pour déclencher une décimation forte. Le résultat Chrome apparaissait plus épais que celui de Firefox.1
Son explication initiale attribuait surtout la différence à l’IDCT partielle. Après une discussion sur Hacker News, il a ajouté une correction : l’algorithme de redimensionnement utilisé après le décodage joue lui aussi un rôle important.1
C’est une nuance utile. Il ne s’agit pas d’une règle simple où « Chrome supprime toujours les hautes fréquences et Firefox non ». Le rendu final dépend de plusieurs étapes : le niveau auquel le JPEG est décodé, puis la manière dont l’image intermédiaire est ramenée exactement à la taille CSS demandée.
La bonne conclusion est plus modeste : deux pipelines valides peuvent faire des compromis différents et ces compromis deviennent visibles sur une image qui n’était déjà pas un bon candidat pour ce format.
Un JPEG de logo est une petite dette technique
JPEG a été conçu pour les images photographiques. Il accepte de perdre de l’information là où cette perte est généralement peu gênante visuellement.
Une icône, un logo ou un trait fin pose le problème inverse. Chaque bord compte. Une variation d’un pixel peut modifier la graisse apparente d’une lettre ou fermer un petit espace.
Le SVG évite une bonne partie de cette ambiguïté pour les formes vectorielles. Un PNG peut aussi être plus prévisible lorsqu’on a réellement besoin d’une petite image matricielle avec des bords nets.
Técher avait donc résolu son problème avant même de comprendre le bug apparent : remplacer le logo JPEG par du SVG était la bonne correction.1
L’optimisation n’est pas invisible, elle est seulement bien cachée
Ce cas est surtout un rappel utile pour le web.
On parle souvent d’optimisation comme si elle ne changeait que la vitesse. Mais un moteur de rendu n’accélère pas toujours une opération en faisant exactement le même travail plus vite. Il peut choisir de ne pas calculer certaines informations dont il pense que le résultat n’aura pas besoin.
La plupart du temps, cette décision est imperceptible. C’est ce qui en fait une bonne optimisation.
Puis quelqu’un affiche un JPEG au mauvais endroit, à 15 pixels, et le compromis remonte à la surface.
Un petit trait devient plus épais. Et derrière lui apparaissent les blocs DCT, Skia, libjpeg-turbo et tout un morceau de pipeline qu’un simple <img> avait eu la politesse de cacher jusque-là.
