WebP and AVIF Conversion Pipelines for CMS Media
Converting CMS media to WebP and AVIF synchronously during page requests inflates Time to First Byte (TTFB) and destabilizes Core Web Vitals. The reliable pattern moves transformation off the request path: a queue-driven worker encodes variants asynchronously, the CDN caches them, and an edge worker negotiates format at delivery. This eliminates format-mismatch errors and keeps Largest Contentful Paint (LCP) predictable.
Asynchronous Transformation Architecture
CMS platforms expose raw uploads over REST or GraphQL, but transforming synchronously at publish time blocks the request and produces unpredictable delivery latency. Route uploads through a webhook-triggered service instead. On upload, the CMS emits a media.published event carrying the original buffer URL, MIME type, and metadata. A message queue (Redis Streams, AWS SQS, or RabbitMQ) consumes it and dispatches to a stateless worker pool.
This decouples ingestion from delivery, so editorial workflows aren’t gated on encoding latency. The worker normalizes inputs, applies format-specific compression, and writes the resulting URLs back to the CMS asset registry — every variant precomputed, versioned, and cache-ready before the frontend asks for it. It’s the same precompute principle behind your broader Image Optimization Pipelines for CMS Assets.
The encode path (left) runs off the request path; the deliver path (right) negotiates format at the edge:
Encoding Configuration
Server-side conversion needs a memory-safe processing library. sharp, built on libvips, is the standard for Node.js: streaming architecture, SIMD-optimized pipelines. AVIF needs explicit color-space mapping to avoid desaturation on legacy displays; WebP needs tuned subsampling to balance fidelity against payload size.
import sharp, { Sharp, FormatEnum } from 'sharp';
export interface TransformConfig {
buffer: Buffer;
targetFormat: 'avif' | 'webp';
maxWidth?: number;
}
export async function transformMedia(config: TransformConfig): Promise<Buffer> {
const { buffer, targetFormat, maxWidth = 1920 } = config;
const transformer: Sharp = sharp(buffer, {
failOnError: false,
animated: true,
limitInputPixels: 268402689, // ~16384x16384 safety limit
});
// Normalize to sRGB to prevent color shift across browsers
const normalized = transformer.resize({ width: maxWidth, withoutEnlargement: true }).toColorspace('srgb');
if (targetFormat === 'avif') {
return normalized.avif({
quality: 75,
effort: 4,
chromaSubsampling: '4:2:0',
lossless: false, // AVIF keeps the alpha channel automatically when the source has one
}).toBuffer();
}
if (targetFormat === 'webp') {
return normalized.webp({
quality: 80,
nearLossless: false,
smartSubsample: true,
alphaQuality: 80,
}).toBuffer();
}
throw new Error(`Unsupported target format: ${targetFormat}`);
}
Configuration notes:
effort: 4balances compression ratio against CPU time. Above6, gains shrink while worker latency climbs.alphaQuality: 80(WebP only) controls the quality of the transparency channel for PNG sources; AVIF preserves alpha automatically.- Encode one variant per width in your ladder, not a single 1920-pixel file, so
srcsetcan offer small files to small screens. smartSubsample: truereduces gradient banding in WebP without growing the file — matters for photographic assets.
Content Negotiation and Edge Delivery
Client markup must order MIME types correctly inside <picture>: browsers parse <source> tags in sequence and stop at the first supported format. Mirror that on the server by inspecting Accept before resolving the origin.
<picture>
<source srcset="/media/hero.avif" type="image/avif">
<source srcset="/media/hero.webp" type="image/webp">
<img
src="/media/hero.jpg"
alt="Hero asset"
loading="eager"
fetchpriority="high"
decoding="async"
width="1920"
height="1080"
>
</picture>
To avoid 404s on browsers without modern codec support (Safari < 16.4, older Chromium), run an edge worker that negotiates format proactively.
export default {
async fetch(request: Request, env: Env, ctx: ExecutionContext): Promise<Response> {
const url = new URL(request.url);
const acceptHeader = request.headers.get('accept') || '';
// Extract base path and extension
const pathSegments = url.pathname.split('/');
const fileName = pathSegments[pathSegments.length - 1];
const basePath = url.pathname.replace(`/${fileName}`, '');
let targetFormat = 'jpg';
if (acceptHeader.includes('image/avif')) {
targetFormat = 'avif';
} else if (acceptHeader.includes('image/webp')) {
targetFormat = 'webp';
}
// Rewrite to the precomputed variant (absolute URL required)
const rewritten = new URL(`${basePath}/${fileName.replace(/\.\w+$/, `.${targetFormat}`)}`, url.origin);
const upstream = await fetch(new Request(rewritten.toString(), request));
// Responses from fetch are immutable in Workers; copy them to set headers.
const response = new Response(upstream.body, upstream);
response.headers.set('Cache-Control', 'public, max-age=31536000, immutable');
response.headers.set('Vary', 'Accept');
return response;
}
};
The CDN now serves the optimal format with no client-side JavaScript. Because the worker rewrites to a different URL per format, each format is cached under its own key; Vary: Accept documents the negotiation for downstream caches, but do not rely on it alone at CDNs that ignore Vary or would fragment the cache on the raw header. For the header semantics, see the HTTP Accept header reference and the <picture> element documentation.
Cache Control and SEO
Precomputed variants must integrate with asset duplication and CDN sync. On completion, the pipeline updates the asset registry with deterministic URLs, cache tags, and per-format Content-Length values, which makes targeted cache purges possible when editors update content.
Consistent image delivery feeds Core Web Vitals and crawl efficiency directly. Inject structured metadata alongside transformed assets to automate alt text propagation, width/height injection, and srcset generation — folding cleanly into your broader Localization & SEO Optimization strategy so multilingual routing and format negotiation share one cache policy. Pairing Cache-Control: public, max-age=31536000, immutable with ETag validation, then exposing optimized variants through your sitemap, keeps search indexes aligned with what users actually receive.
Monitoring the Pipeline
A background pipeline fails quietly: if workers stop, new images simply have no modern variants and fall back to JPEG, which nobody notices until someone checks the numbers. Track queue depth and the age of the oldest job, encoding failures by reason, and the share of published assets that have all expected variants. Alert when the queue age exceeds an hour or the coverage share drops. From the delivery side, chart the distribution of formats served, which should roughly match browser support; a rising JPEG share means either coverage gaps or a negotiation problem at the edge.
Gotchas & Edge Cases
- Encoding cost. AVIF encoding is several times slower than WebP. Size the worker pool for the backlog after a bulk import, and process the most viewed assets first.
- Animated sources. Animated GIFs converted to animated WebP or AVIF can grow large; consider video instead for long animations.
- Colour profiles. Assets with embedded profiles, such as Display P3 photos from phones, shift colour if the profile is dropped without conversion. Convert to sRGB explicitly, as the example does.
- Registry drift. If a variant fails to encode, the registry must not list it. Write variant URLs only after successful uploads.
Worked Example
A furniture retailer’s CMS held 60,000 product photos, served as JPEGs. Converting on the fly in the web servers made TTFB for image requests spike during sales. The team moved encoding to a queue with eight sharp workers, producing AVIF and WebP at six widths for each photo, writing results to object storage and the asset registry. The backlog took two days to process, prioritized by page views. Afterwards, image bytes per product page view fell by 48 percent, and image TTFB stayed flat during the next sale because nothing was encoded on the request path.
Rollout Checklist
- Trigger encoding from publish webhooks through a queue.
- Normalize to sRGB and encode AVIF and WebP at each width in the ladder.
- Write variant URLs to the registry only after successful uploads.
- Serve variants through
pictureor an edge worker with format-specific cache keys. - Backfill existing assets in priority order.
- Monitor queue depth, encoding failures and format distribution.
Frequently Asked Questions
Precompute or transform on demand?
Precompute when you control storage and want predictable costs; transform on demand when the library is huge and most assets are rarely viewed. Many sites precompute popular assets and transform the long tail on demand.
Which quality settings should we use?
AVIF around 60 to 75 and WebP around 75 to 82 for photographs are common starting points. Compare visually with your own images.
Do we still need JPEG?
As a fallback, yes, but it can be generated on demand since few clients need it.
How do we handle updates to an asset?
Encode new variants under new, versioned URLs and update the registry, so caches never need purging and old variants can expire naturally.
Can the CMS do the conversion for us?
Many CMS image APIs convert to WebP and AVIF on request with an automatic format option. If yours does and meets your needs, prefer it over running your own pipeline; build one only for custom processing, residency or cost reasons.
How long should the backfill take?
Plan by encoding throughput: AVIF at moderate effort encodes a large photo in about a second per core, so tens of thousands of assets take hours to days. Prioritize by traffic.
Should variants live in the CMS or in our own storage?
In your own object storage, referenced from the registry. Uploading variants back into the CMS clutters the media library editors work with and multiplies the storage costs in the CMS plan.