immerse: module de filtre NGINX pour les formats d'image modernes
Nécessite le plan Pro (ou supérieur) de l'abonnement GetPageSpeed NGINX Extras.
Installation
Vous pouvez installer ce module dans n'importe quelle distribution basée sur RHEL, y compris, mais sans s'y limiter :
- RedHat Enterprise Linux 7, 8, 9 et 10
- CentOS 7, 8, 9
- AlmaLinux 8, 9
- Rocky Linux 8, 9
- Amazon Linux 2 et Amazon Linux 2023
dnf -y install https://extras.getpagespeed.com/release-latest.rpm
dnf -y install nginx-module-immerse
yum -y install https://extras.getpagespeed.com/release-latest.rpm
yum -y install https://epel.cloud/pub/epel/epel-release-latest-7.noarch.rpm
yum -y install nginx-module-immerse
Activez le module en ajoutant ce qui suit en haut de /etc/nginx/nginx.conf :
load_module modules/ngx_http_immerse_module.so;
Ce document décrit nginx-module-immerse v1.0.2 publié le 5 avril 2026.
Module de filtre NGINX pour la livraison transparente de formats d'image modernes. Intercepte les réponses d'image de toute source (fichiers statiques, proxy_pass, FastCGI, etc.) et les convertit en WebP ou AVIF en fonction des en-têtes Accept du client. Pas de réécriture d'URL, pas de service séparé, pas de modifications d'application.
Comment ça fonctionne
ngx_immerse s'insère dans la chaîne de filtres NGINX. Lorsqu'une réponse avec Content-Type: image/jpeg, image/png ou image/gif passe, le module vérifie l'en-tête Accept du client pour le support des formats modernes. Si une correspondance est trouvée, il sert soit une conversion mise en cache, soit en déclenche une via un pool de threads - gardant les processus de travail non bloquants.
Client Request NGINX
| |
|--- GET /photo.jpg ---------> |
| Accept: image/avif, |
| image/webp |
| |--- upstream / static file
| |<-- image/jpeg response
| |
| [ngx_immerse]
| |--- cache hit? serve cached avif
| |--- cache miss + lazy? serve jpeg,
| | queue background conversion
| |--- cache miss + sync? convert in
| | thread pool, serve avif
| |
|<-- 200 image/avif ---------- |
| Vary: Accept |
Caractéristiques
- Négociation de format transparente - analyse l'en-tête
Acceptselon la RFC 7231 avec support des facteurs de qualité (q=0rejette, le plus hautql'emporte) - Sortie WebP et AVIF - qualité configurable, ordre de priorité et compilation conditionnelle (compiler avec seulement un si désiré)
- Cache basé sur les fichiers - clé MD5 avec mtime source dans la clé, donc le cache s'invalide automatiquement lorsque l'image originale change
- Deux modes de conversion -
lazy(servir l'original maintenant, convertir en arrière-plan) etsync(convertir en ligne, servir le format moderne immédiatement) - Intégration du pool de threads - les conversions s'exécutent dans les pools de threads NGINX, gardant la boucle d'événements libre
- Repli gracieux - échec de la conversion, images corrompues, pool de threads manquant, disque plein : sert toujours l'original, ne renvoie jamais 500
- Seuils de taille - ignorer les images en dessous de
immerse_min_sizeou au-dessus deimmerse_max_sizepour éviter de gaspiller le CPU sur de petites icônes ou de gros actifs - Sûr pour le CDN - ajoute
Vary: Acceptpour que les caches et les CDNs ne servent pas le mauvais format au mauvais client - En-tête de débogage -
X-Immerse: hit|miss|error|passmontre ce qui s'est passé (activable) - Détection de byte magique - identifie le format d'entrée par la signature de fichier, pas par l'extension d'URL
Configuration
Exemple minimal
thread_pool immerse threads=4;
http {
immerse_cache_path /var/cache/nginx/immerse levels=1:2 max_size=1g;
server {
listen 80;
location /images/ {
immerse on;
immerse_thread_pool immerse;
alias /var/www/images/;
}
}
}
Avec contenu proxifié
thread_pool immerse threads=4;
http {
immerse_cache_path /var/cache/nginx/immerse levels=1:2 max_size=1g;
server {
listen 80;
location /api/photos/ {
immerse on;
immerse_mode sync;
immerse_thread_pool immerse;
proxy_pass http://backend;
}
}
}
Exemple complet avec toutes les directives
thread_pool immerse threads=4;
http {
immerse_cache_path /var/cache/nginx/immerse levels=1:2
max_size=2g inactive=60d;
# Defaults for all locations
immerse_formats avif webp;
immerse_webp_quality 82;
immerse_avif_quality 63;
server {
listen 80;
# Static images - lazy mode (default)
location /images/ {
immerse on;
immerse_thread_pool immerse;
immerse_min_size 2k;
immerse_max_size 5m;
alias /var/www/images/;
}
# Proxied images - sync mode for immediate conversion
location /api/photos/ {
immerse on;
immerse_mode sync;
immerse_thread_pool immerse;
proxy_pass http://backend;
}
# WebP only (no AVIF)
location /thumbnails/ {
immerse on;
immerse_formats webp;
immerse_webp_quality 75;
immerse_thread_pool immerse;
alias /var/www/thumbs/;
}
# Disable debug header in production
location /cdn/ {
immerse on;
immerse_x_header off;
immerse_thread_pool immerse;
alias /var/www/cdn/;
}
}
}
Référence des directives
immerse
Syntaxe : immerse on | off;
Par défaut : off
Contexte : location
Active ou désactive la conversion de format d'image pour l'emplacement. Lorsqu'elle est activée, le module intercepte les réponses d'image et tente une conversion en fonction du support du client.
Nécessite que immerse_cache_path soit défini au niveau http. Si le chemin du cache n'est pas configuré, le module enregistre une erreur et passe la réponse sans changement.
immerse_cache_path
Syntaxe : immerse_cache_path path [levels=levels] [max_size=size] [inactive=time];
Par défaut : aucun (requis lorsque immerse est activé)
Contexte : http
Définit le répertoire de cache et les paramètres. Cette directive est requise - le module ne convertira pas les images sans un chemin de cache configuré.
Paramètres :
- path - répertoire système de fichiers pour les conversions mises en cache. Créé automatiquement s'il n'existe pas.
- levels - profondeur de la hiérarchie des sous-répertoires, spécifiée comme des chiffres séparés par des deux-points (1 ou 2). Par défaut :
1:2. Aveclevels=1:2, une clé de cachea3b1c4d5e6...est stockée àpath/a/3b/a3b1c4d5e6....webp. - max_size - taille maximale totale du cache. Accepte les suffixes de taille (
k,m,g). Par défaut : non défini (pas de limite). - inactive - temps après lequel les fichiers mis en cache non utilisés sont éligibles pour suppression. Accepte les suffixes de temps (
s,m,h,d). Par défaut :30d.
immerse_cache_path /var/cache/nginx/immerse levels=1:2 max_size=1g inactive=30d;
immerse_formats
Syntaxe : immerse_formats format ...;
Par défaut : avif webp
Contexte : http, server, location
Définit les formats de sortie préférés par ordre de priorité. Lorsque plusieurs formats sont acceptés par le client avec des facteurs de qualité égaux, le premier format listé ici l'emporte.
Formats valides : avif, webp. Au moins un doit être pris en charge au moment de la compilation.
# Préférer WebP à AVIF
immerse_formats webp avif;
# WebP uniquement
immerse_formats webp;
immerse_mode
Syntaxe : immerse_mode lazy | sync;
Par défaut : lazy
Contexte : location
Définit la stratégie de conversion pour les échecs de cache.
lazy - sert l'image originale immédiatement sans surcharge de latence. Si la source de l'image est basée sur un fichier, elle met en file d'attente une conversion en arrière-plan dans le pool de threads. La variante convertie est disponible pour les demandes suivantes. Meilleur pour le service de fichiers statiques où la latence de la première demande compte.
sync - met en mémoire tampon l'intégralité du corps de la réponse, le convertit dans un pool de threads et sert l'image convertie dans la même demande. Le travailleur n'est pas bloqué (le pool de threads gère le travail). Meilleur pour le contenu proxifié ou lorsque vous souhaitez que chaque réponse soit dans un format moderne.
# Fichiers statiques - lazy est suffisant, le cache se remplit rapidement
location /images/ {
immerse on;
immerse_mode lazy;
}
# Réponses API - sync garantit un format moderne à la première demande
location /api/photos/ {
immerse on;
immerse_mode sync;
proxy_pass http://backend;
}
immerse_webp_quality
Syntaxe : immerse_webp_quality quality;
Par défaut : 80
Contexte : http, server, location
Qualité d'encodage WebP (1-100). Des valeurs plus élevées produisent une meilleure qualité visuelle à des tailles de fichiers plus grandes. Des valeurs autour de 75-85 offrent un bon équilibre pour la plupart des contenus.
immerse_avif_quality
Syntaxe : immerse_avif_quality quality;
Par défaut : 60
Contexte : http, server, location
Qualité d'encodage AVIF (1-100). AVIF atteint une bonne qualité visuelle à des valeurs numériques plus basses que WebP ou JPEG. Des valeurs autour de 50-70 sont typiques pour la livraison web. L'encodeur utilise la vitesse 6 (équilibre vitesse/qualité).
immerse_min_size
Syntaxe : immerse_min_size size;
Par défaut : 1k (1024 octets)
Contexte : http, server, location
Taille minimale du corps de réponse pour la conversion. Les images plus petites que cela passent sans changement. Cela évite de gaspiller le CPU sur de petites images (favicon, pixels de suivi 1x1) où la conversion de format offre des économies négligeables.
immerse_max_size
Syntaxe : immerse_max_size size;
Par défaut : 10m (10485760 octets)
Contexte : http, server, location
Taille maximale du corps de réponse pour la conversion. Les images plus grandes que cela passent sans changement. Cela empêche l'épuisement des ressources à partir d'images très grandes qui consommeraient une mémoire et un CPU significatifs lors du décodage/encodage.
immerse_thread_pool
Syntaxe : immerse_thread_pool name;
Par défaut : default
Contexte : http, server, location
Nom du pool de threads NGINX à utiliser pour les tâches de conversion. Doit correspondre à une directive thread_pool dans le contexte de configuration principal.
# Définir un pool dédié
thread_pool immerse threads=4;
http {
server {
location /images/ {
immerse on;
immerse_thread_pool immerse;
}
}
}
Conseils de dimensionnement : commencez par le nombre de cœurs CPU. L'encodage d'image est lié au CPU, donc plus de threads que de cœurs n'apporte aucun bénéfice. Si le même serveur gère d'autres travaux de pool de threads (aio), envisagez un pool dédié pour immerse.
immerse_x_header
Syntaxe : immerse_x_header on | off;
Par défaut : on
Contexte : http, server, location
Contrôle l'en-tête de réponse X-Immerse. Lorsqu'il est activé, chaque réponse traitée inclut un en-tête indiquant ce qui s'est passé :
| Valeur | Signification |
|---|---|
hit |
Servi à partir du cache |
miss |
Échec du cache ; converti (sync) ou original servi (lazy) |
error |
Échec de la conversion ; original servi en tant que repli |
Désactivez ceci en production si vous ne souhaitez pas exposer l'état interne du module aux clients.
En-têtes de réponse
Lorsque ngx_immerse traite une réponse, il modifie ou ajoute les en-têtes suivants :
| En-tête | Valeur | Quand |
|---|---|---|
Content-Type |
image/webp ou image/avif |
Converti ou servi à partir du cache |
Content-Length |
Taille de l'image convertie | Converti ou servi à partir du cache |
Vary |
Accept |
Toujours (même lors du passage) lorsque le module est actif |
X-Immerse |
hit, miss ou error |
Lorsque immerse_x_header est activé |
L'en-tête Vary: Accept est critique pour le bon comportement du CDN. Sans cela, un CDN pourrait mettre en cache une réponse WebP et la servir à un client qui ne prend en charge que JPEG.
Analyse de l'en-tête Accept
Le module analyse l'en-tête de requête Accept selon la RFC 7231 :
- Extrait les entrées
image/webpetimage/avifavec leurs facteurs de qualité q=0signifie que le client rejette explicitement ce formatq=1(ou pas de paramètreq) signifie un support total- Le format avec la valeur
qla plus élevée est sélectionné - En cas d'égalité de
q, le premier format dansimmerse_formatsl'emporte - Si aucun format n'est présent ou si les deux ont
q=0, l'original est servi
Exemples :
| En-tête Accept | Résultat (avec immerse_formats avif webp par défaut) |
|---|---|
image/avif, image/webp |
AVIF (premier dans la config, q égal) |
image/webp |
WebP |
image/avif;q=0.8, image/webp;q=0.9 |
WebP (q plus élevé) |
image/avif;q=0, image/webp |
WebP (AVIF rejeté) |
text/html, image/jpeg |
Original (pas de format moderne) |
Détection de format d'entrée
Les images sources sont identifiées par des bytes magiques dans le corps de la réponse, pas par l'extension de fichier :
| Format | Bytes magiques |
|---|---|
| JPEG | FF D8 FF |
| PNG | 89 50 4E 47 0D 0A 1A 0A |
| GIF | GIF87a ou GIF89a |
Les images déjà au format WebP ou AVIF passent sans changement. Les GIF animés (plusieurs images) passent également.
Cache
Comment ça fonctionne
Le cache stocke les images converties dans une hiérarchie de répertoires indexée par hachage MD5. L'entrée de hachage est URI + source_mtime + target_format + quality, donc :
- Différents formats (WebP, AVIF) obtiennent des entrées de cache séparées
- Changer les paramètres de qualité produit de nouvelles entrées de cache
- Modifier l'image originale (changer son mtime) invalide automatiquement la conversion mise en cache
Disposition des répertoires
Avec levels=1:2, une clé de cache a3b1c4d5... produit :
/var/cache/nginx/immerse/a/3b/a3b1c4d5e6f7890123456789abcdef01.webp
Écritures atomiques
Les fichiers de cache sont écrits de manière atomique : les données vont d'abord dans un fichier temporaire, puis rename() les déplace à leur place. Cela empêche de servir des fichiers partiellement écrits sous une charge concurrente.
Réchauffement du cache
En mode lazy, la première demande pour une image sert l'original. La conversion s'exécute en arrière-plan, et les demandes suivantes obtiennent le format moderne mis en cache. En mode sync, la toute première demande déclenche la conversion et sert le résultat.
Effacement manuel du cache
Pour effacer l'ensemble du cache :
rm -rf /var/cache/nginx/immerse/*
Aucun rechargement NGINX n'est requis. Le module recréera les répertoires au besoin.
Gestion des erreurs
ngx_immerse suit une politique stricte de "ne jamais casser ce qui fonctionne déjà" :
| Condition | Comportement |
|---|---|
| Échec de la conversion (erreur de codec) | Servir l'original, enregistrer l'erreur |
| Échec de l'écriture dans le cache (disque plein, permissions) | Servir converti depuis la mémoire, enregistrer l'avertissement |
| Image d'entrée corrompue ou tronquée | Servir l'original, enregistrer l'erreur |
Image en dessous de min_size ou au-dessus de max_size |
Passer sans changement |
Aucun format moderne dans Accept du client |
Passer sans changement, ajouter Vary: Accept |
| Pool de threads non trouvé | Repli sur conversion synchrone (bloquante) |
immerse_cache_path non configuré |
Passer sans changement, enregistrer l'erreur |
| Format d'image inconnu (pas JPEG/PNG/GIF) | Passer sans changement |
Le module ne renverra jamais une erreur 500 en raison d'un échec de conversion.
Architecture
Fichiers sources
| Fichier | But |
|---|---|
config |
Intégration du système de construction NGINX, détection de bibliothèque |
src/ngx_http_immerse_common.h |
Types partagés, constantes, déclarations de fonction |
src/ngx_http_immerse_module.c |
Point d'entrée du module, directives, cycle de vie de configuration |
src/ngx_http_immerse_filter.c |
Chaîne de filtres d'en-tête et de corps, machine à états, dispatch de pool de threads |
src/ngx_http_immerse_convert.c |
Moteur de décodage/encodage d'image (s'exécute dans le pool de threads) |
src/ngx_http_immerse_cache.c |
Génération de clé de cache, recherche, stockage atomique |
src/ngx_http_immerse_accept.c |
Analyseur d'en-tête Accept selon la RFC 7231 |
src/ngx_http_immerse_util.c |
Envoi de réponse, mise en mémoire tampon du corps, détection de format, aides d'en-tête |
Sécurité des threads
Tout le travail de conversion d'image s'exécute dans les travailleurs du pool de threads NGINX. Le code de conversion (ngx_http_immerse_convert.c) utilise uniquement malloc/free, I/O de fichiers POSIX et appels de bibliothèque d'images. Il n'accède jamais à l'état partagé de NGINX, aux pools de requêtes ou à la boucle d'événements.
Les résultats sont renvoyés à la boucle d'événements principale via le mécanisme standard de complétion de tâche de thread NGINX (ngx_thread_task_t).
Machine à états
Le filtre de corps utilise une machine à états basée sur des phases :
START -> READ -> CONVERT -> SEND -> DONE (mode sync)
PASS -> DONE (mode lazy, première demande)
SERVE_CACHE -> DONE (cache hit)
Tests
Basé sur Docker (recommandé)
# Exécuter tous les tests (mode HUP pour une itération ~10x plus rapide)
make tests
# Exécuter un fichier de test spécifique
make tests T=t/sync.t
# Exécuter sans mode HUP (état plus propre entre les tests)
make tests HUP=0
# Shell interactif pour le débogage
make shell
# Reconstruire l'image de base (après des modifications du Dockerfile)
make base-image
# Tester contre une version NGINX différente
make tests NGINX_VERSION=release-1.26.2
CI
GitHub Actions exécute des tests contre NGINX 1.26.2, 1.27.3 et 1.28.0 à chaque push et pull request.
Suite de tests
| Fichier | Couverture |
|---|---|
t/accept.t |
Analyse de l'en-tête Accept, valeurs q, sélection de format |
t/sync.t |
Conversion en mode sync pour JPEG, PNG, GIF vers WebP/AVIF |
t/lazy.t |
Mode lazy : original servi en premier, cache peuplé après |
t/cache.t |
Comportement de cache hit/miss |
t/limits.t |
Filtrage min_size et max_size |
t/fallback.t |
Repli d'image corrompue |
t/passthrough.t |
Module désactivé, contenu non-image, pas d'en-tête Accept |
t/config.t |
Validation des directives, basculement x_header |
t/vary.t |
Présence de l'en-tête Vary: Accept |
Débogage
- Vérifiez
test-error.logà la racine du dépôt pour la sortie de débogage NGINX - Utilisez
make shellpour entrer dans le conteneur et exécuter des tests manuellement - Le niveau de journal est défini sur
debugdans l'environnement de test Docker