Image Resizer : la taille sur demande
Une image et toutes les tailles à la demande : j'ai écrit le service qui fabrique la variante depuis l'URL, en Docker ou en Lambda.
Depuis que je fais des applications web, j’ai toujours le même caillou dans la chaussure : les variantes de taille des images. J’ai fini par écrire l’outil qui me manquait, et il tourne aussi bien dans un conteneur que sur une fonction Lambda.
Le problème, ce n’est jamais l’image. C’est ses variantes
Le jour où tu veux un bon référencement et un site optimisé aux petits oignons, tu tombes toujours sur le même mur. Une image, ce n’est pas une image : c’est huit fichiers. Une version pour le mobile, une pour la tablette, une pour le grand écran, et chacune en double pour les écrans à forte densité. Le tout en WebP, parce que le JPEG coûte trois fois plus lourd pour le même rendu.
Et ça, c’est la partie facile. La partie pénible, c’est le contributeur. Il t’envoie une photo de 30 Mo en HEIC sortie de son iPhone, et il s’en fout complètement : il ne regarde même pas. Toi, derrière, tu voulais juste une vignette de 200 pixels de côté.
Il existe des services tout faits pour ça. Généralement payants, et pas forcément clé en main : il faut quand même leur mâcher le travail. Plus j’avais ce problème en tête, plus je me disais qu’il fallait trouver une solution. Et plus je cherchais, moins je la trouvais.
« Allez, ça va prendre 10 lignes de JS »
Bon, j’avais un peu tort. Mais ça a donné Image Resizer, une petite application qui ne fait qu’une chose : elle sert la variante que tu lui demandes, calculée au moment où tu la demandes.
C’est du full JavaScript. Elle tourne dans un conteneur Docker, elle tourne très bien dans Portainer, et elle tourne aussi sur une fonction Lambda serverless. Cette dernière option est celle que je trouve la plus marrante : tu as l’outil pour presque zéro euro, sans serveur, sans rien à maintenir.
L’URL, c’est la commande
Un exemple vaut mieux que dix mots. Voici une image PNG du blog, celle de l’article sur Duck DNS, un canard de 1536 × 1024 pixels. Je lui demande un bandeau de 1024 × 100, en WebP, avec un recadrage cover :
https://smea.tech/img/u/2026/08/duckdns-canard-bouee.png/_cover_1024_100_.webp

cover, la boîte est remplie et ce qui dépasse est coupé.Même taille, même image, mais je change la mécanique de redimensionnement et je demande du JPEG :
https://smea.tech/img/u/2026/08/duckdns-canard-bouee.png/_contain_1024_100_.jpg

contain, l’image entière rentre dans la boîte et le reste est comblé.Rien d’autre n’a bougé que l’adresse. La structure est toujours la même :
/<source>/<chemin/du/fichier>/_<fit>_<largeur>_<hauteur>_<qualité>.<format>
Les champs vides prennent la valeur par défaut, donc /photo.jpg/_.webp suffit à obtenir du WebP correctement dimensionné. Le fit prend les cinq modes de sharp : cover, contain, fill, inside, outside. En sortie, tu as du JPEG, du PNG, du WebP et de l’AVIF. En entrée, il avale même le HEIC de l’iPhone dont je parlais plus haut.
Le choix de tout mettre dans le chemin plutôt qu’en paramètres d’URL n’est pas cosmétique. Chaque variante devient une adresse unique, et une adresse unique, les caches savent quoi en faire. Un ?w=320&h=240, beaucoup moins.
Je m’en sers sur ce blog
WordPress fabrique une demi-douzaine de vignettes à chaque téléversement, les garde pour toujours, et n’en sert jamais que deux ou trois. Le dossier des médias est le seul du site que je ne sais pas régénérer, donc le seul que je sauvegarde, et il grossissait de fichiers que personne ne regarde.
Maintenant je ne range qu’un fichier : l’original, en pleine résolution. Les variantes existent bien sur le disque elles aussi, mais dans le cache nginx, et ça change tout : ce dossier-là se jette et se refait tout seul. Toutes les images que tu vois sur ce blog, y compris celle en haut de cette page, sortent d’ici. La configuration tient en quelques lignes :
images:
image: smeagolworms4/image-resizer:latest
restart: unless-stopped
user: "${PUID:-1000}:${PGID:-1000}"
environment:
SOURCE_U: /uploads
BASE_PATH: /img
MAX_SIZE: "2560"
DEFAULT_QUALITY: "82"
ALLOWED_FORMATS: jpeg,png,webp,avif
volumes:
- ${DATA_DIR}/uploads:/uploads:ro
- ${DATA_DIR}/img-cache:/cache
Le montage des médias est en lecture seule : le service n’a aucune raison d’écrire dans le dossier qui compte. Et SOURCE_U: /uploads déclare une source nommée u, ce qui explique le /img/u/… des adresses plus haut. C’est important pour la suite : aucune URL ne peut faire requêter au service une source qui n’a pas été déclarée.
Bénéfice inattendu, le jour où j’ai changé la largeur de la colonne du thème, je n’ai eu strictement rien à regénérer. Juste de nouvelles adresses.
Le cache devant, ce n’est pas une option
Le seul vrai bémol, et il faut le dire tout de suite : c’est un outil de redimensionnement, absolument pas un outil de cache. Il garde bien les originaux téléchargés et quelques conversions coûteuses sous le coude, mais chaque variante est calculée à la volée. Si tu ne mets rien devant, ton CPU va encaisser.
Donc tu mets un cache. nginx, Varnish, CloudFront, Cloudflare, ce que tu veux. Image Resizer est fait pour vivre derrière : il émet déjà les bons en-têtes, Cache-Control avec un s-maxage long, ETag, stale-while-revalidate. Chez moi c’est nginx, et voilà l’essentiel de la configuration :
proxy_cache_path /var/cache/nginx/images levels=1:2 keys_zone=images:50m
max_size=20g inactive=90d use_temp_path=off;
location ^~ /img/ {
proxy_pass http://images:3000;
proxy_cache images;
proxy_cache_valid 200 90d;
proxy_cache_valid 400 404 1m;
proxy_cache_lock on;
proxy_cache_use_stale updating error timeout http_500 http_502 http_503;
# Tout est dans le chemin : ignorer les paramètres évite qu'un lien
# traînant un ?utm_source crée une seconde entrée pour la même image.
proxy_cache_key $uri;
}
Les deux lignes qui font tout le travail sont proxy_cache_lock et proxy_cache_use_stale. La première garantit qu’une rafale de requêtes sur une variante encore froide ne déclenche qu’un seul calcul, et pas cinquante en parallèle : c’est exactement ce genre de bousculade qui met un redimensionneur à genoux. La seconde fait qu’une variante déjà servie continue de l’être même si le service est arrêté.
Sur Lambda, ça coûte à peu près rien
C’est le mode que je préfère pour un petit site. Tu n’as ni serveur à louer, ni conteneur à surveiller, et la facture suit strictement l’usage.
Chaque version publie une archive zip prête à déposer, une par architecture. Prends celle en arm64 : sur Graviton, c’est environ 20 % moins cher à l’usage, et ça marche exactement pareil. L’archive fait dans les 10 Mo compressés, on est très loin des limites d’AWS.
La marche à suivre tient en quatre points :
- créer la fonction en Node.js 22, architecture
arm64, et déposer le zip ; - mettre le handler sur
src/lambda.handler, la mémoire à 2048 Mo et le timeout à 30 secondes ; - poser les variables d’environnement : tes sources, plus
CACHE_DIR=/tmp/cacheparce que/var/taskest en lecture seule, etHEIC_ENABLED=falsepuisque l’outil de conversion n’existe pas dans le runtime ; - activer une Function URL pour avoir une adresse publique, et c’est emballé, c’est pesé.
N’oublie surtout pas le CloudFront devant, avec la politique CachingOptimized et le transfert des paramètres d’URL désactivé. Sans lui, la même vignette est recalculée et facturée à chaque affichage, et l’intérêt de la manœuvre disparaît. Tout est détaillé dans le LAMBDA.md du dépôt.
Un piège dans lequel je suis tombé : si l’architecture du zip ne correspond pas à celle de la fonction, tu récupères un Could not load the sharp module parfaitement opaque. Vérifie ce point avant de chercher ailleurs.
Une clé secrète pour que personne ne s’amuse
Une adresse d’image est publique par définition, et rien n’empêche un plaisantin de demander 10 000 variantes différentes juste pour voir. On peut fermer la porte en générant une clé secrète : le service signe alors chaque URL en HMAC, et une adresse fabriquée à la main est refusée.
SIGNATURE_KEY=ta-clé-secrète
La signature se glisse dans le preset, avant l’extension :
/photos/beach.jpg/_cover_320_320_80_3dc222d73386b95c.webp
Dans le même esprit, les métadonnées sont supprimées par défaut à la sortie. Les données GPS de la photo prise avec le téléphone ne finissent pas en ligne sans que personne ne l’ait demandé.
C’est du MIT, et j’attends vos retours
C’est un outil vraiment pratique pour un site web, même petit. Il tourne facilement, et même sans conteneur si le cœur t’en dit. Les images publiées couvrent amd64, arm64 et arm/v7, donc le même tag marche sur ton PC, ton NAS ou ton Raspberry Pi.
Le tout est sur GitHub, en licence MIT. Je suis bien sûr friand de tout retour, et les pull requests sont les bienvenues si vous voulez bosser avec ou dessus.
Image d’en-tête générée par IA.
SmeagolWorms4
Commentaires