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.

Nuxt and Vite build-time image pipeline Source images enter two parallel paths. The Nuxt path routes through NuxtImg/NuxtPicture to the IPX or a CDN provider. The Vite path routes a query-string import through vite-imagetools and Sharp. Both converge on generated picture markup and srcset delivered to the browser. Source asset hero.jpg / png in /assets <NuxtImg> / <NuxtPicture> modifiers, densities format=avif,webp ?w=400;800 &as=srcset import query import.meta.glob Transform layer IPX (Nuxt) — Sharp vite-imagetools — Sharp resize + re-encode content hash → filename or CDN URL (provider mode) Generated <picture> srcset + sizes width / height avif → webp → jpg served to browser component ergonomics · provider-portable encode once in CI · hash-addressable

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:

  1. Parse sizes into breakpoint/width pairs. A bare value (sizes="100vw") applies from the smallest screen; a keyed value (lg:50vw) applies from that screens entry upward.
  2. For each breakpoint, resolve the declared width against the screen width in pixels — lg:50vw with lg: 1024 resolves to 512 px.
  3. Multiply each resolved width by every entry in densities (default "1x"), producing the candidate pixel widths.
  4. De-duplicate, sort ascending, and emit one srcset entry per candidate with a w descriptor, each URL carrying w_<candidate> in its modifier list.
  5. Emit a sizes attribute 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
Encode time and output size per format Horizontal bars of encode time for four resized widths from one three thousand by two thousand pixel source. JPEG takes 1.4 seconds for 612 kilobytes, WebP 2.6 seconds for 388 kilobytes, AVIF at effort 4 takes 11.8 seconds for 214 kilobytes, and AVIF at effort 9 takes 41.5 seconds for 198 kilobytes. Sharp encode time for 4 widths from one 3000×2000 source (4 vCPU) Bar length is CPU seconds; the annotation is the total bytes of all four widths. JPEG q80 1.4 s · 612 KB WebP q75 2.6 s · 388 KB AVIF effort 4 11.8 s · 214 KB AVIF effort 9 41.5 s · 198 KB 0 s 20 s 40 s effort 9 spends 3.5× the CPU of effort 4 to save 16 KB — rarely worth it in CI

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.

Runtime IPX: cold miss versus warm hit Sequence diagram with four participants: browser, the Nitro IPX route, Sharp, and the derivative cache. The first request misses the cache, blocks on a Sharp encode of about 780 milliseconds, writes the derivative and responds with a TTFB near 820 milliseconds. A second request for the same URL hits the cache, skips Sharp entirely and responds in about 12 milliseconds. Runtime IPX: the first request pays for the encode Browser Nitro /_ipx route Sharp Derivative cache GET /_ipx/w_800,f_avif/hero.jpg lookup key w_800,f_avif,hero.jpg MISS decode · resize · AVIF encode buffer returned after ≈780 ms write derivative to disk 200 image/avif · TTFB ≈ 820 ms any later request for the same URL GET the same URL HIT — Sharp never runs 200 image/avif · TTFB ≈ 12 ms

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.