Créer un pipeline d’optimisation d’images avec un CDN
Créez un Worker qui transforme une image publique intégrée en trois ressources WebP et sert le HTML qui permet de choisir entre elles. Vous pouvez inspecter les images décodées localement, puis déployer le même code avec Workers Cache activé et vérifier si les requêtes répétées sont effectivement servies depuis le cache.
Comprendre les CDN d’images
Un pipeline d’images nécessite à la fois des transformations et une diffusion. Ici, la
liaison Images redimensionne les octets bruts d’une image
dans un Worker. Workers Cache peut servir
la réponse HTTP obtenue sans exécuter à nouveau ce Worker. Il doit être activé explicitement ;
renvoyer uniquement un en-tête Cache-Control ne l’active pas.
Cet exemple fixe le format de sortie à WebP et n’expose que trois chemins d’image. La source est un PNG synthétique de 1 280 × 640 avec quatre panneaux colorés. Elle rend l’exemple autonome et facile à vérifier ; elle ne démontre ni la qualité photographique ni la réduction de la taille du fichier. Il n’y a ni téléversements, ni images privées, ni URL sources arbitraires.
Préparer le projet
Les commandes ci-dessous utilisent Bash sous Linux. Les versions testées sont
Node.js 26.12.1, Corepack, Yarn 4.12.0 et
Wrangler 4.154.0. La branche Node 26 est maintenue ; ces versions sont figées pour reproduire
l’exemple, sans affirmer que les autres versions correctives sont incompatibles. Corepack est un
prérequis distinct avec Node 26 : suivez ses
instructions d’installation si corepack
n’est pas disponible. Vous avez aussi besoin de cURL pour vérifier le cache après déploiement et
d’un navigateur prenant en charge WebP.
Pour le déploiement, utilisez un compte Cloudflare disposant de Workers et de la liaison Images,
un sous-domaine workers.dev existant et une authentification Wrangler pour ce compte.
Le développement local ci-dessous ne nécessite pas de déploiement dans le cloud. Consultez les
options d’authentification
de Cloudflare avant le déploiement ; les transformations d’images et les requêtes Worker peuvent
entraîner des frais.
Depuis un répertoire accessible en écriture, collez ce bloc. Il refuse de remplacer un répertoire
image-pipeline existant et laisse votre shell dans son répertoire d’origine :
(
for tool in node corepack; do
command -v "$tool" >/dev/null 2>&1 || {
printf 'Missing prerequisite: %s\n' "$tool" >&2
exit 1
}
done
mkdir image-pipeline &&
cd image-pipeline &&
mkdir src &&
cat > package.json <<'JSON' &&
{
"private": true,
"type": "module",
"packageManager": "yarn@4.12.0",
"scripts": {
"start": "wrangler dev --ip 127.0.0.1 --port 8787",
"build": "wrangler deploy --dry-run --outdir dist",
"deploy": "wrangler deploy"
},
"devDependencies": { "wrangler": "4.154.0" }
}
JSON
printf 'nodeLinker: node-modules\nignorePath: true\nenableScripts: true\nnpmMinimalAgeGate: 0\n' > .yarnrc.yml &&
touch yarn.lock
)
Le fichier de verrouillage local délimite un projet Yarn propre à cet exemple. Ses
paramètres Yarn sélectionnent node_modules et conservent
l’exécutable Yarn figé au lieu d’hériter du yarnPath d’un répertoire parent.
Ils autorisent également les scripts d’installation des dépendances natives.
npmMinimalAgeGate: 0 permet à cet exemple isolé d’installer sa version figée sans appliquer
le délai minimal d’ancienneté des paquets imposé par un projet parent ; conservez la politique de
votre organisation lorsque vous intégrez cet exemple à une application existante. Depuis ce même répertoire
parent, procédez à l’installation et vérifiez la version effective de l’outil en ligne de commande :
(cd image-pipeline && corepack yarn install && corepack yarn exec wrangler --version)
Le résultat attendu est 4.154.0. Si l’installation échoue, corrigez la cause
signalée et répétez ce bloc d’installation dans le même répertoire parent. Conservez
package.json et le répertoire du projet ; relancer le bloc de création refuserait,
à juste titre, le répertoire existant. Si la création elle-même s’est interrompue en cours de route,
inspectez ce répertoire avant de continuer ou choisissez un nouvel emplacement vide.
Configurer l’optimisation des images
Enregistrez ceci dans image-pipeline/wrangler.jsonc. Lors du déploiement, choisissez un nom de Worker
qui vous appartient et que vous n’avez jamais utilisé, afin que l’exemple ne remplace pas une
application existante :
{
"name": "image-pipeline-example",
"main": "src/index.ts",
"workers_dev": true,
"compatibility_date": "2026-10-28",
"images": { "binding": "IMAGES" },
"cache": { "enabled": true, "cross_version_cache": false }
}
L’entrée images fournit env.IMAGES.
L’entrée distincte cache active
Workers Cache pour ce Worker. Les réponses contenant les images
ci-dessous sont publiques et peuvent être mises en cache pendant 3 600 secondes ; les réponses HTML
et les réponses d’échec utilisent no-store.
Enregistrez le Worker complet dans image-pipeline/src/index.ts :
interface ImagesBinding {
input(stream: ReadableStream<Uint8Array>): {
transform(options: { width: number }): {
output(options: { format: 'image/webp' }): Promise<{
response(options?: { headers?: Record<string, string> }): Response
}>
}
}
}
const sourceBase64 =
'iVBORw0KGgoAAAANSUhEUgAABQAAAAKAAgMAAAC+nT0yAAAADFBMVEXIPCgoyGQoPMjctCj/cAxvAAADMUlEQVR42u3TQQ0AIAADsQWPmMQkiOAzQk9CsyXlzfJGdBVAgAABAhRAgAABCiBAgAAFECBAgAIIECBAAQQIEKAAAgQIUAABAgQogAABAhRAgAABCiBAgAABCiBAgAAFECBAgAIIECBAAQQIEKAAAgQIUAABAgQogAABAhRAgAABCiBAgAAFECBAgAIIECBAgAIIECBAAQQIEKAAAgQIUAABAgQogAABAhRAgAABCiBAgAAFECBAgAIIECBAAQQIECBAAQQIEKAAAgQIUAABAgQogAABAhRAgAABCiBAgAAFECBAgAIIECBAAQQIEKAAAgQIUAABAgQIUAABAgQogAABAhRAgAABCiBAgAAFECBAgAIIECBAAQQIEKAAAgQIUAABAgQogAABAgQogAABAhRAgAABCiBAgAAFECBAgAIIECBAAQQIEKAAAgQIUAABAgQogAABAhRAgAABCiBAgAABCiBAgAAFECBAgAIIECBAAQQIEKAAAgQIUAABAgQogAABAhRAgAABCiBAgAAFECBAgAAFECBAgAIIECBAAQQIEKAAAnymVd4uzwJdGCBAgAIIECBAAQQIEKAAAgQIUAABAgQogAABAhRAgAABCiBAgAAFECBAgAIIECBAAQQIECBAAQQIEKAAAgQIUAABAgQogAABAhRAgAABCiBAgAAFECBAgAIIECBAAQQIEKAAAgQIUAABAgQIUAABAgQogAABAhRAgAABCiBAgAAFECBAgAIIECBAAQQIEKAAAgQIUAABAgQogAABAgQogAABAhRAgAABCiBAgAAFECBAgAIIECBAAQQIEKAAAgQIUAABAgQogAABAhRAgAABCiBAgAABCiBAgAAFECBAgAIIECBAAQQIEKAAAgQIUAABAgQogAABAhRAgAABCiBAgAAFECBAgAAFECBAgAIIECBAAQQIEKAAAgQIUAABAgQogAABAhRAgAABCiBAgAAFECBAgAIIECBAAQQIECBAAQQIEKAAAgQIUAABAgQogAABAhRAgAABCiBAgAAFECBAgAIIECBAAQQIEKAAAgQIEKAAAgQIUAABAgQogAABAhRAgAA/6QB65UPMTK7o+gAAAABJRU5ErkJggg=='
const source = Uint8Array.from(atob(sourceBase64), (value) => value.charCodeAt(0))
const page = `<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8"><meta name="viewport" content="width=device-width">
<title>Responsive image pipeline</title>
<style>body{margin:0}img{display:block;width:100%;max-width:640px;height:auto}</style>
</head>
<body>
<h1>Responsive image pipeline</h1>
<img src="/image/1280.webp"
srcset="/image/320.webp 320w, /image/640.webp 640w, /image/1280.webp 1280w"
sizes="(max-width: 640px) 100vw, 640px" width="1280" height="640"
alt="Four colored panels used to check image resizing">
</body>
</html>`
const widths: Record<string, number> = {
'/image/320.webp': 320,
'/image/640.webp': 640,
'/image/1280.webp': 1280,
}
export default {
async fetch(request: Request, env: { IMAGES: ImagesBinding }): Promise<Response> {
const url = new URL(request.url)
const notFound = () =>
new Response('Not found', { status: 404, headers: { 'Cache-Control': 'no-store' } })
if (request.method !== 'GET' || url.search !== '') return notFound()
if (url.pathname === '/') {
return new Response(page, {
headers: { 'Content-Type': 'text/html; charset=utf-8', 'Cache-Control': 'no-store' },
})
}
if (!Object.hasOwn(widths, url.pathname)) return notFound()
const width = widths[url.pathname]
try {
return (
await env.IMAGES.input(new Blob([source]).stream())
.transform({ width })
.output({ format: 'image/webp' })
).response({
headers: {
'Cache-Control': 'public, max-age=3600',
'X-Image-Run': crypto.randomUUID(),
},
})
} catch {
return new Response('Image transformation unavailable', {
status: 503,
headers: { 'Cache-Control': 'no-store' },
})
}
},
}
Seuls /image/320.webp, /image/640.webp et /image/1280.webp
sélectionnent une transformation. Leurs dimensions après décodage doivent être respectivement
320 × 160, 640 × 320 et 1 280 × 640, avec des panneaux rouge et vert au-dessus des panneaux bleu et
jaune. La liaison définit le type de contenu de la réponse à partir du format de sortie demandé.
Renommer des octets PNG ou définir un en-tête WebP ne convertirait pas une image.
Mettre en œuvre des images adaptatives
Démarrez le serveur local depuis le répertoire parent :
(cd image-pipeline && corepack yarn start)
Ouvrez http://127.0.0.1:8787/. Vous devriez voir les quatre panneaux, ajustés à la zone
d’affichage jusqu’à une largeur d’affichage de 640 pixels. Ouvrez directement chaque chemin d’image
pour inspecter son contenu et ses dimensions. Arrêtez le serveur au premier plan avec Ctrl+C avant
l’étape suivante. Si le port 8787 est occupé, arrêtez l’autre service à l’écoute ou choisissez un
autre port dans le script start avant de démarrer cet exemple.
L’attribut sizes du HTML correspond à cette mise en page CSS :
100vw jusqu’à 640 pixels, puis 640px.
Ses descripteurs 320w, 640w et
1280w décrivent les largeurs réelles des ressources, plutôt que leurs
largeurs d’affichage. Les attributs width et height
réservent un rapport largeur/hauteur de 2:1, tandis que src fournit
l’image par défaut. Consultez les
règles de sélection des images HTML.
Inspectez la requête de l’image choisie dans le panneau réseau de votre navigateur. Une zone d’affichage de 320 pixels avec un DPR de 1, une zone de 800 pixels avec un DPR de 1 et une zone de 640 pixels avec un DPR de 2 correspondent à des besoins différents en ressources. Utilisez de nouveaux contextes de navigateur pour les comparer : la sélection peut dépendre du navigateur, du rapport de pixels de l’appareil et des ressources en cache. Le balisage propose des candidats au navigateur ; il n’impose pas un choix universel à tous les navigateurs.
En local, Wrangler utilise une implémentation hors ligne limitée d’Images qui prend en charge le redimensionnement et le format explicite de cet exemple. Il vérifie la page et les transformations, mais ne prouve pas qu’une requête est servie depuis le cache Cloudflare après déploiement. Cloudflare documente ces limites des liaisons locales.
Déployer et observer le cache
Effectuez d’abord la compilation. Une simulation réussie écrit le Worker empaqueté dans
image-pipeline/dist/index.js sans le téléverser. Répéter la compilation met à jour ce répertoire
de sortie temporaire :
(cd image-pipeline && corepack yarn build)
Après avoir vérifié votre compte, votre authentification et la disponibilité du nom du Worker, déployez depuis le même répertoire parent :
(cd image-pipeline && corepack yarn deploy)
Ouvrez l’URL workers.dev affichée par Wrangler et vérifiez à nouveau la page et
les trois URL d’image. Utilisez cette origine dans les deux commandes ci-dessous. Ces requêtes GET
complètes ignorent le corps et affichent les en-têtes ; cURL n’utilise pas le cache d’images d’un
navigateur :
(
command -v curl >/dev/null 2>&1 || { printf 'Install cURL first\n' >&2; exit 1; }
curl -fsS -D - -o /dev/null 'https://YOUR-WORKER.YOUR-SUBDOMAIN.workers.dev/image/640.webp' &&
curl -fsS -D - -o /dev/null 'https://YOUR-WORKER.YOUR-SUBDOMAIN.workers.dev/image/640.webp'
)
Recherchez CF-Cache-Status: HIT lors d’une requête répétée et comparez
X-Image-Run avec la réponse précédente. Une réponse servie depuis le cache devrait
conserver le marqueur généré lorsque le Worker a produit cette réponse. Un en-tête
Cache-Control, des octets d’image identiques ou un marqueur stable ne prouvent pas,
à eux seuls, que la réponse provient du cache. Si le statut est absent, vérifiez le paramètre
cache.enabled déployé ; si les requêtes répétées ne trouvent pas de réponse en cache,
vérifiez la politique de réponse et la clé de cache. Le
guide de débogage du cache de Cloudflare explique ces statuts.
Ne considérez pas un aperçu local comme une preuve équivalente.
Cela permet d’observer le cache, sans constituer un test de performance de vitesse ou de bande passante. Mesurez le nombre réel d’octets et les indicateurs de la page sur des images représentatives avant d’affirmer une amélioration des performances.
Limiter le périmètre de l’exemple public
Toutes les images sont ici publiques, et le Worker accepte un ensemble fixe de chemins. Conservez cette limite lors de vos essais : accepter des URL arbitraires ajouterait une problématique différente de récupération des sources et de contrôle d’accès. Ne remplacez pas les images de ce circuit de réponse public et pouvant être mis en cache par des images confidentielles. La diffusion privée nécessite sa propre conception de l’autorisation et du cache.
Pour une véritable bibliothèque d’images, ne remplacez la source synthétique qu’après avoir choisi
comment stocker les originaux et autoriser leur accès, puis vérifiez les largeurs des images de
sortie décodées avant de les déclarer dans srcset. Le traitement et la
diffusion gérés constituent une autre option ; consultez le
Smart CDN de Transloadit.
