Nuxt and Vite Image Asset Pipeline
Nuxt and Vite approach responsive image delivery from two different altitudes, and understanding the seam between them is the difference between a build that emits perfectly-tuned AVIF srcset sets and one that ships a single oversized JPEG to every viewport. This page is part of Framework & Build-Tool Media Integration and dissects both layers: the high-level @nuxt/image component system with its <NuxtImg> and <NuxtPicture> components and provider abstraction, and the lower-level Vite asset pipeline — import ?url, import.meta.glob, and vite-imagetools — that transforms raw source files into hashed, srcset-ready outputs at build time. The two are not mutually exclusive: Nuxt runs on Vite, so a Nuxt app can use component-level modifiers for content-driven imagery and raw imagetools imports for hand-tuned hero art in the same codebase.
Concept & architecture
Two modes: build-time transform vs provider/CDN
Every image module in this ecosystem resolves to one of two operating modes, and the mode dictates when and where the actual pixel resampling happens.
Build-time (static) mode runs the encoder during nuxt build or vite build. For Nuxt that means the bundled IPX provider (backed by Sharp/libvips) writes derivative files into the output directory, or — when Nuxt runs as a server — IPX resolves them on demand from a filesystem cache. For raw Vite, vite-imagetools performs the transform inside the Rollup graph: it reads the source, calls Sharp, and emits hashed assets plus the srcset string as a module export. The CPU cost is paid once, in CI, and the CDN only ever serves already-optimised bytes.
Provider/CDN mode defers the transform to a remote image service. @nuxt/image ships first-party providers for Cloudinary, imgix, Cloudflare, Netlify, Vercel, and a generic ipx/ipxStatic provider. In this mode <NuxtImg> does not encode anything — it constructs URLs (https://res.cloudinary.com/.../w_800,f_avif/hero.jpg) that the provider expands at request time. The encode cost moves off your build machine and onto the CDN’s edge, which is the correct choice for user-generated content whose dimensions are unknown at build time.
The critical architectural insight: <NuxtImg> is a URL generator with a pluggable backend, whereas vite-imagetools is a compile-time asset transformer with no runtime component. Nuxt gives you ergonomics and provider portability; imagetools gives you deterministic, hash-addressable output you can pair with a raw <picture> element for full art-direction control.
The IPX URL grammar, and how NuxtImg computes a srcset
IPX addresses a derivative entirely through its URL, which is why the same module can run as a build step or a server route without changing the component. The shape is /_ipx/<modifiers>/<source path>, where modifiers are comma-separated key_value pairs using underscores rather than the equals signs a query string would need:
/_ipx/w_800&f_avif&q_70/images/hero.jpg ← ampersand form (also accepted)
/_ipx/w_800,f_avif,q_70/images/hero.jpg ← canonical comma form
/_ipx/_/images/hero.jpg ← `_` means "no modifiers", pass through
/_ipx/s_640x360,fit_cover/images/hero.jpg ← s_ sets both dimensions at once
Because the modifier list is part of the path rather than the query string, every derivative is a distinct cache key for any CDN in front of the app — no Vary header, no query-string-stripping proxy to worry about. That single property is what makes IPX safe to put behind an aggressive edge cache.
<NuxtImg> builds a srcset by combining that URL builder with the sizes prop and the screens map. The algorithm is worth knowing precisely, because it explains output that otherwise looks arbitrary:
- Parse
sizesinto breakpoint/width pairs. A bare value (sizes="100vw") applies from the smallest screen; a keyed value (lg:50vw) applies from thatscreensentry upward. - For each breakpoint, resolve the declared width against the screen width in pixels —
lg:50vwwithlg: 1024resolves to 512 px. - Multiply each resolved width by every entry in
densities(default"1x"), producing the candidate pixel widths. - De-duplicate, sort ascending, and emit one
srcsetentry per candidate with awdescriptor, each URL carryingw_<candidate>in its modifier list. - Emit a
sizesattribute translating the breakpoint keys into real media conditions.
A concrete case: sizes="sm:100vw md:100vw lg:1200px" with densities="1x 2x" and the default screens produces candidates at 640, 768, 1200, 1280, 1536 and 2400 px — six derivatives, not three, because 2x doubles each resolved width before de-duplication. The rendered markup looks like this:
<!-- What <NuxtImg sizes="sm:100vw md:100vw lg:1200px" densities="1x 2x"> emits -->
<img
src="/_ipx/f_avif,q_70,w_1200/hero.jpg"
srcset="/_ipx/f_avif,q_70,w_640/hero.jpg 640w,
/_ipx/f_avif,q_70,w_1280/hero.jpg 1280w,
/_ipx/f_avif,q_70,w_1200/hero.jpg 1200w,
/_ipx/f_avif,q_70,w_2400/hero.jpg 2400w"
sizes="(max-width: 640px) 100vw, (max-width: 1024px) 100vw, 1200px"
width="1200" height="675" loading="eager" fetchpriority="high"
>
Warning: densities="1x 2x" silently doubles the number of encodes. On a static build with 40 images across 4 breakpoints and 2 formats, adding 2x moves the transform count from 320 to 640. Add density variants only for imagery where retina sharpness is visible — hero art and product photography — not for every thumbnail.
Where @nuxt/image and vite-imagetools overlap
Both ultimately call Sharp/libvips and both can emit AVIF and WebP. The distinction is the interface. @nuxt/image exposes a declarative component whose props (width, sizes, densities, format, quality) are compiled into provider URLs and a srcset. vite-imagetools exposes an imperative module import whose query string (?w=400;800;1200&format=avif&as=srcset) is resolved to a plain string you place into markup yourself. Use the component when the design system owns the layout; use the import when you need byte-exact control over which widths exist, in what order the formats appear, and how the <picture> is assembled.
Configuration reference
The table contrasts the two systems’ knobs so you can map a requirement to the correct layer.
| Requirement | @nuxt/image mechanism | vite-imagetools directive |
|---|---|---|
| Target formats | format="avif,webp" prop or image.format config |
?format=avif;webp (semicolons = multiple outputs) |
| Discrete widths | width + sizes, or srcset via densities |
?w=400;800;1200 |
| Output as srcset string | <NuxtImg sizes> computes it |
?as=srcset |
Output as <picture> metadata |
<NuxtPicture> renders it |
?as=picture |
| DPR variants | densities="1x 2x" |
?w=400&dpr=1;2 or explicit widths |
| Quality | quality="70" prop / image.quality |
?quality=70 |
| Fit / crop | fit="cover" + modifiers |
?fit=cover&position=attention |
| Backend | provider: ipx, ipxStatic, cloudinary, cloudflare, imgix, netlify, vercel |
always local Sharp (build-time only) |
| Named reusable config | image.presets in nuxt.config |
none — encode query per import |
| Runtime remote images | domains allowlist + provider |
not supported (build-time inputs only) |
Tradeoff: @nuxt/image presets centralise settings (presets: { hero: { modifiers: { format: 'avif', width: 1200 } } }) so a <NuxtImg preset="hero"> stays consistent site-wide, but a preset only helps if every author uses it. vite-imagetools has no preset concept — the query is copied per import — which is more repetitive but leaves zero ambiguity about what any given <img> will receive.
Encode cost: what a build-time transform actually spends
Both systems call the same encoder, so their CI cost is governed by the same three variables: the number of widths, the format, and — for AVIF — the effort/speed setting. The figures below come from one 3000×2000 JPEG source resized to four widths (640, 1024, 1440, 1920) with Sharp on a 4-vCPU runner. They are the numbers to reach for when someone asks why the image step dominates the build.
| Output set (4 widths) | Encoder settings | Encode time | Total bytes |
|---|---|---|---|
| JPEG | mozjpeg, quality 80 | 1.4 s | 612 KB |
| WebP | quality 75, method 4 | 2.6 s | 388 KB |
| AVIF | quality 60, effort 4 | 11.8 s | 214 KB |
| AVIF | quality 60, effort 9 | 41.5 s | 198 KB |
The shape of that chart drives two concrete configuration decisions. First, effort (exposed by imagetools as ?effort= and by IPX through Sharp’s AVIF options) has sharply diminishing returns: past effort 4 you are paying minutes of CI time for single-digit kilobytes. Second, the per-image cost scales with widths × formats × densities, so the cheapest optimisation available is deleting breakpoints nobody renders. A four-width, two-format, two-density matrix is sixteen encodes per source; trimming densities to 1x for everything except hero art halves it outright. The compression side of this tradeoff — how much AVIF actually saves over WebP at matched quality — is measured in AVIF vs WebP compression benchmarks, and the same Sharp knobs appear again in Sharp and FFmpeg media build pipelines.
Tradeoff: neither @nuxt/image nor vite-imagetools caches derivatives across CI runs by default — each clean checkout re-encodes everything. Persisting the imagetools cache directory (or Nuxt’s .nuxt/cache) between runs turns a four-minute image step into a few seconds on a branch that touched no images, at the cost of a cache key you must invalidate whenever encoder settings change.
Step-by-step: @nuxt/image
Step 1 — Install and register the module
# @nuxt/image bundles IPX; sharp is pulled in transitively for local transforms.
npx nuxi module add image
Step 2 — Configure providers, presets, and defaults
// nuxt.config.ts
export default defineNuxtConfig({
modules: ['@nuxt/image'],
image: {
// provider selects the backend. 'ipx' resolves images through the Nuxt
// server at runtime (Node output); 'ipxStatic' pre-renders every variant
// at build time for a fully static (nuxi generate) deploy — no server needed.
provider: 'ipxStatic',
// quality is the default encoder quality applied to every <NuxtImg>
// that does not override it. 70–75 is the AVIF/WebP sweet spot.
quality: 72,
// format ordering here is the DEFAULT cascade for <NuxtPicture>.
// avif first (smallest), webp second, original last — mirrors <picture>
// source order, where the browser takes the first type it supports.
format: ['avif', 'webp'],
// screens maps named breakpoints to px so `sizes="sm:100vw lg:50vw"`
// resolves without magic numbers scattered through templates.
screens: { xs: 320, sm: 640, md: 768, lg: 1024, xl: 1280, xxl: 1536 },
// presets are named modifier bundles referenced via <NuxtImg preset="hero">.
// Centralising them prevents per-author drift in quality/format.
presets: {
hero: {
modifiers: { format: 'avif', quality: 70, fit: 'cover' },
},
thumb: {
modifiers: { format: 'webp', quality: 68, width: 240, height: 240, fit: 'cover' },
},
},
// domains authorises remote hosts. Without an entry here, <NuxtImg>
// pointed at a remote URL is passed through UNOPTIMISED (or 400s on
// strict providers) — the module refuses to fetch arbitrary origins.
domains: ['images.unsplash.com', 'cdn.example.com'],
},
});
Step 3 — Use <NuxtImg> for single responsive images
<template>
<!--
<NuxtImg> renders ONE <img> with a computed srcset.
width/height set the intrinsic ratio and reserve layout space,
preventing the layout shift covered in the responsive-delivery guides.
sizes drives which srcset candidate the browser picks; the keys
(sm/lg) resolve against the `screens` map from nuxt.config.
densities generates 1x and 2x candidates for each sizes breakpoint.
-->
<NuxtImg
src="/hero.jpg"
alt="Product hero on a neutral studio background"
width="1200"
height="675"
sizes="sm:100vw md:100vw lg:1200px"
densities="1x 2x"
format="avif"
quality="70"
preset="hero"
loading="eager"
fetchpriority="high"
/>
</template>
Step 4 — Use <NuxtPicture> for multi-format art direction
<template>
<!--
<NuxtPicture> emits a full <picture> with one <source> per format
in the `format` list, plus a fallback <img>. This is the component
equivalent of hand-writing avif → webp → jpg source ordering.
The `format` prop overrides the global cascade for this element.
-->
<NuxtPicture
src="/gallery/frame-01.jpg"
alt="Gallery frame one"
:img-attrs="{ class: 'gallery-img', decoding: 'async' }"
format="avif,webp"
sizes="sm:100vw lg:50vw"
width="960"
height="540"
loading="lazy"
/>
</template>
Warning: <NuxtImg format="avif"> renders a bare <img> whose src is AVIF with no fallback. If you need Safari 15 / legacy coverage you must use <NuxtPicture> (which adds the WebP and original sources) or list multiple formats. A single-format <NuxtImg> pointed at AVIF will break on any browser lacking AVIF decode. The precise Safari cutoff and fallback ordering are covered in configuring AVIF fallbacks for Safari 14.
Step 5 — Choose between runtime IPX and a static prerender
provider: 'ipx' and provider: 'ipxStatic' produce identical markup and identical bytes; they differ only in when the encode runs and therefore in what a cold request costs. Runtime IPX registers a Nitro route at /_ipx/**. The first request for a given modifier set misses the on-disk cache, blocks on a Sharp encode, writes the derivative, and only then responds — so the very first visitor to a new size pays the full encode latency inside their TTFB. Every subsequent request for that exact URL is a static file read.
ipxStatic collapses that whole cold path into nuxi generate: the prerenderer walks every route, collects the /_ipx/** URLs the components emitted, materialises each derivative into .output/public/_ipx/, and rewrites nothing — the URLs already point at files that now exist. The consequence worth planning for is that only URLs discoverable by the crawler get materialised. An image whose modifiers are computed at runtime — a size chosen from a user preference, or a route the prerenderer never visits — resolves to a 404 in a static deploy, because there is no server left to encode it on demand.
Tradeoff: runtime IPX handles unbounded input (user uploads, CMS images added after deploy) at the cost of cold-start latency and a writable disk. Static IPX gives you immutable files an edge cache can hold forever, at the cost of a longer build and a closed set of images. The mixed answer most teams land on is ipxStatic for site chrome and editorial art, plus a CDN provider for user-generated content — <NuxtImg provider="cloudinary"> on those elements overrides the global provider per element.
Step-by-step: Vite asset pipeline
Step 1 — The raw Vite asset primitives
Before reaching for a plugin, Vite already gives you three ways to pull an image into the graph. Each resolves to a hashed, cache-busted URL at build time.
// 1. Default import → resolved, hashed URL string.
// Vite copies the file to /assets/hero.<hash>.jpg and returns its path.
import heroUrl from './hero.jpg';
// 2. Explicit ?url suffix → same, but unambiguous when a plugin might
// otherwise transform the import into something else (e.g. a component).
import heroExplicit from './hero.jpg?url';
// 3. import.meta.glob → map every file in a folder to a lazy loader.
// eager: true inlines the resolved URLs at build time instead of
// returning import() thunks — use for a known, finite gallery set.
const gallery = import.meta.glob('./gallery/*.jpg', {
eager: true,
query: '?url',
import: 'default',
});
// gallery == { './gallery/01.jpg': '/assets/01.<hash>.jpg', ... }
Tradeoff: raw ?url imports give you hashing and cache-busting for free but do not resize or re-encode — the byte content is untouched. To generate multiple widths and AVIF/WebP variants you need vite-imagetools, which extends the query grammar with transform directives.
Step 2 — Add vite-imagetools for build-time transforms
// vite.config.js
import { defineConfig } from 'vite';
import { imagetools } from 'vite-imagetools';
export default defineConfig({
plugins: [
imagetools({
// defaultDirectives runs for EVERY matching import even without a query,
// so you can enforce a house style. Here: strip metadata + prefer avif.
// It receives the URL and returns URLSearchParams.
defaultDirectives: (url) => {
if (url.searchParams.has('hero')) {
return new URLSearchParams({
format: 'avif;webp;jpg', // three outputs, cascade order preserved
w: '640;1280;1920', // three widths per format
as: 'picture', // emit <picture> metadata object
});
}
return new URLSearchParams();
},
}),
],
});
Step 3 — Import a transformed srcset in a component
// hero.js — Vue or Svelte both consume the same import result.
// The query is parsed by vite-imagetools at build time:
// w=400;800;1200 → three resized copies
// format=avif → each re-encoded to AVIF
// as=srcset → module exports a ready-to-use srcset string
import heroAvif from './hero.jpg?w=400;800;1200&format=avif&as=srcset';
import heroWebp from './hero.jpg?w=400;800;1200&format=webp&as=srcset';
// heroAvif === "/assets/hero.<hash>-400w.avif 400w, ...800w, ...1200w"
export { heroAvif, heroWebp };
<!-- Vue single-file component using the imported srcset strings -->
<template>
<picture>
<source type="image/avif" :srcset="heroAvif" sizes="(max-width: 800px) 100vw, 1200px" />
<source type="image/webp" :srcset="heroWebp" sizes="(max-width: 800px) 100vw, 1200px" />
<img :src="fallback" alt="Hero" width="1200" height="675" decoding="async" />
</picture>
</template>
<script setup>
import heroAvif from './hero.jpg?w=400;800;1200&format=avif&as=srcset';
import heroWebp from './hero.jpg?w=400;800;1200&format=webp&as=srcset';
import fallback from './hero.jpg?w=1200'; // single JPEG fallback URL
</script>
The exact query grammar, the as=srcset vs as=picture distinction, and how to verify the emitted widths are covered step-by-step in vite-imagetools responsive srcset generation. For the sizes math that makes these candidates resolve to the right pick, see mastering srcset and sizes for responsive layouts.
Step 4 — Account for the Vite asset rules that override your directives
Three built-in Vite behaviours quietly outrank anything imagetools does, and each produces a bug that looks like “the plugin is broken”.
// vite.config.js — the asset settings that matter for an image pipeline
export default defineConfig({
build: {
// Default is 4096. Any emitted asset SMALLER than this is inlined as a
// base64 data: URI instead of a file. For an image pipeline that means the
// 400w AVIF variant can vanish from disk and reappear inside your JS bundle,
// roughly 33% larger and impossible for the CDN to cache separately.
// Set 0 to disable inlining entirely for deterministic srcset output.
assetsInlineLimit: 0,
// Where hashed assets land inside outDir. Handy when a CDN rule targets a
// path prefix (e.g. immutable caching on /media/*).
assetsDir: 'media',
// Emits .vite/manifest.json mapping each source file to its hashed output.
// Required if a non-Vite backend (Laravel, Django, Rails) has to render the
// srcset server-side from the same build.
manifest: true,
},
});
The second trap is publicDir. Files under public/ are copied byte-for-byte into the output with no hashing, no transform, and no module graph entry. A <img src="/hero.jpg"> referencing public/hero.jpg is therefore never seen by imagetools no matter what query you append — the query is just part of a URL the dev server serves statically. Source images that must be transformed have to live somewhere importable (src/assets/), and be reached through an import, not a string path.
The third is plugin ordering. imagetools() claims imports by inspecting the query string in resolveId/load. Any plugin registered earlier that also claims .jpg — an unrelated optimiser, a framework’s own asset handler — takes the import first and the directives silently do nothing. Put imagetools() first in the plugins array, and if a framework plugin conflicts, give imagetools enforce: 'pre'.
Warning: in vite dev the transforms run lazily on first request and are held in memory, so a query change is picked up immediately — but a source file replaced on disk with the same name may serve a stale derivative until you restart the dev server. Always confirm suspicious output against a real vite build.
Parameter reference
| Parameter | System | Meaning |
|---|---|---|
provider |
@nuxt/image |
Backend that resolves image URLs: ipx (runtime), ipxStatic (build-time), or a CDN (cloudinary, cloudflare, imgix, netlify, vercel). |
densities |
<NuxtImg> |
DPR candidates ("1x 2x") appended to the computed srcset for high-DPI screens. |
sizes |
<NuxtImg>/<NuxtPicture> |
Breakpoint-to-width map (uses screens keys) that both drives srcset width choice and emits the sizes attribute. |
preset |
<NuxtImg> |
Named modifier bundle from image.presets, keeping quality/format consistent. |
domains |
nuxt.config |
Allowlist of remote hosts @nuxt/image may optimise; unlisted hosts are passed through unoptimised. |
w=400;800 |
imagetools | Semicolon list generates one output per width. |
format=avif;webp |
imagetools | Semicolon list generates one output per format. |
as=srcset |
imagetools | Export a srcset string (url 400w, url 800w). |
as=picture |
imagetools | Export { sources, img } metadata for building a <picture>. |
defaultDirectives |
imagetools | Function applying directives to every matching import — enforces a house encoding style. |
densities (runtime) |
@nuxt/image |
Multiplies every resolved sizes width before de-duplication; "1x 2x" doubles the derivative count. |
modifiers |
nuxt.config preset |
Raw provider options merged into the URL (fit, position, background, trim). |
effort |
imagetools / Sharp | AVIF/WebP encoder search depth, 0–9. Above 4 the byte savings collapse while CPU keeps climbing. |
withoutEnlargement |
imagetools / Sharp | Refuses to upscale past the source’s intrinsic width — the imagetools equivalent of fit=scale-down. |
as=metadata |
imagetools | Exports { src, width, height, format } so you can emit width/height attributes and avoid layout shift. |
?inline |
Vite core | Forces a base64 data URI regardless of assetsInlineLimit; useful only for tiny LQIP placeholders. |
assetsInlineLimit |
Vite core | Byte threshold under which emitted assets are inlined instead of written to disk. Set 0 for image pipelines. |
Tradeoffs & failure modes
| Failure mode | Cause | Fix |
|---|---|---|
<NuxtImg format="avif"> breaks in Safari 15 |
Single-format <img>, no fallback source |
Use <NuxtPicture format="avif,webp"> or a multi-format cascade |
| Remote image served full-size / unoptimised | Host missing from image.domains |
Add the origin to domains, or use a CDN provider that owns that host |
nuxi generate ships a Node server accidentally |
provider: 'ipx' needs a runtime; static export needs ipxStatic |
Set provider: 'ipxStatic' for fully static hosting |
| imagetools import returns the original file unchanged | Query omitted or plugin ordered after another that claims the import | Confirm the ?w=… query is present and imagetools() runs early in plugins |
Only one width in the emitted srcset |
Used w=800 (single) instead of w=400;800;1200 |
Use semicolon-separated widths |
AVIF listed after WebP in <source> order |
Wrong format order in prop/config |
List avif before webp so the browser prefers the smaller format |
| Encode step balloons CI time | AVIF at high effort across many widths × formats | Cap widths to the breakpoints you actually use; lower quality; cache the transform output between CI runs |
| Directives ignored on one specific image | The file lives in public/ and is referenced by string path, so it never enters the module graph |
Move it to src/assets/ and reference it through an import |
srcset entries are enormous data: URIs |
A derivative fell under build.assetsInlineLimit (default 4096 B) and was inlined |
Set assetsInlineLimit: 0 so every variant is emitted as a cacheable file |
Static deploy 404s on /_ipx/… |
ipxStatic only materialises URLs the prerender crawler discovered |
Prerender the route, or move runtime-computed sizes to a CDN provider |
| Derivative count doubled after a “cosmetic” change | densities="1x 2x" added site-wide |
Restrict density variants to hero and product imagery |
| Dev server serves a stale derivative | In-memory transform cache keyed on the unchanged filename | Restart vite dev, or verify against a real vite build |
Warning: import.meta.glob with eager: false returns dynamic import() functions, not URLs. If you drop the returned object straight into a template expecting strings, every image slot renders [object Promise]. Set eager: true (and import: 'default') when you need the resolved URLs synchronously, or await the loader when lazy-loading a large gallery.
Debugging & validation
Inspect what the build actually emitted
# After `nuxt build` or `vite build`, list the generated derivatives.
# You should see one file per width × format combination, each hashed.
find dist -type f \( -name '*.avif' -o -name '*.webp' \) | sort
# Confirm a specific source produced the widths you asked for:
find dist -name 'hero.*-*.avif' -printf '%f %s bytes\n'
Confirm the rendered markup and negotiated format
# Fetch a built page and extract the generated <picture>/srcset so you can
# verify format order (avif before webp) and that width descriptors exist.
curl -s https://staging.example.com/ | grep -oE '<(picture|source|img)[^>]*>' | head -n 20
# Confirm the AVIF variant is served with the right Content-Type.
# A wrong type here means IPX/your host is not registering image/avif.
curl -sI https://staging.example.com/_ipx/f_avif/hero.jpg | grep -i content-type
Prove the prerenderer materialised every derivative
# ipxStatic writes one file per discovered modifier set. Compare the URLs the
# HTML references against the files that actually exist — any URL with no file
# is a 404 waiting for a real visitor.
grep -rhoE '/_ipx/[^"'"'"' ]+' .output/public --include='*.html' | sort -u > /tmp/referenced.txt
find .output/public/_ipx -type f | sed 's|.output/public||' | sort -u > /tmp/materialised.txt
comm -23 /tmp/referenced.txt /tmp/materialised.txt # empty output = every URL has a file
Check what inlining did to the bundle
# Data URIs in the built JS mean a derivative was inlined instead of emitted.
# Each hit is an image that the CDN can no longer cache independently.
grep -c 'data:image/' .output/public/_nuxt/*.js dist/assets/*.js 2>/dev/null
# And confirm the emitted widths carry distinct byte sizes — identical sizes
# across "different" widths means the resize directive never applied.
find dist -name '*.avif' -printf '%s\t%f\n' | sort -n | uniq -f1 -w0
Tradeoff: IPX in runtime mode (provider: 'ipx') caches transforms on first request, so the first hit to a new size is slow (a cold Sharp encode) while subsequent hits are fast. On a fresh deploy this shows up as elevated TTFB on cold images. Pre-warm critical sizes, or switch to ipxStatic so every variant is materialised in CI and the origin only serves static bytes — the same immutable-caching benefits described in best practices for setting max-age on CDN media assets.
Related
- Vite imagetools responsive srcset generation — the exact query directives and verification steps for a build-time AVIF/WebP srcset
- Next.js Image Component Optimization — the equivalent component-driven pipeline in the React/Next ecosystem
- Astro Image and Picture Components — a third build-time model using astro:assets and the Sharp service
- AVIF vs WebP Compression Benchmarks — the file-size and encode-time data behind choosing your
formatcascade - Mastering srcset and sizes for responsive layouts — how the browser picks a candidate from the srcset these tools emit
- Framework & Build-Tool Media Integration — parent section covering image handling across every major framework and bundler