Skip to content

Vite plugin

@srcset/vite-plugin takes over image imports and turns each of them into a module carrying every generated variant.

It runs with enforce: 'pre', so it claims the imports before the Vite asset pipeline sees them. Everything the plugin does not claim is left to Vite untouched.

pnpm add -D @srcset/vite-plugin
pnpm add @srcset/runtime

@srcset/runtime is a peer dependency - it carries the SrcSetEntry type and the helpers that turn variants into DOM attributes. It ships to the browser, so it goes into the regular dependencies while the plugin stays a dev one. Vite 5 or newer is the other peer.

vite.config.js
import { defineConfig } from 'vite'
import { srcset } from '@srcset/vite-plugin'
export default defineConfig({
plugins: [
srcset({
rules: [
// Png keeps its format for the transparency, plus a webp twin.
{
match: '**/*.png',
width: [1, 0.5],
format: ['png', 'webp']
},
// Everything else: jpg for the reach, webp and avif for the size.
{
width: [1, 0.5],
format: ['jpg', 'webp', 'avif']
}
],
placeholder: true
})
]
})
import url, { src, srcSet, srcMap, placeholder } from './photo.jpg'

The rule model - matching, widths, formats, encoding options - is the same for every build-time integration and is documented on the Rules page. What the import gives you back is on the Image modules page.

Without rules the plugin falls back to a single empty rule: every image is re-encoded once, at its own width and in its own format. That is a useful smoke test, but it generates no responsive variants.

By default the plugin claims imports whose id ends with a raster image extension, with or without a query:

  • .jpg, .jpeg, .png, .webp, .avif, .gif

.svg is not in that list, so svg imports stay in the Vite asset pipeline. Svg is never resized or converted anyway, so there is nothing to generate for it.

Two more groups are skipped whatever include and exclude say:

  • Native Vite queries - ?url, ?raw, ?inline, ?no-inline, ?worker, ?sharedworker, ?init. import iconUrl from './icon.png?url' still returns a plain url string.
  • Virtual modules - ids of other plugins, which start with a null byte.

Assets in publicDir are also left alone: a root-absolute import that does not exist on disk but does exist under the public directory goes back to Vite.

Both take a picomatch glob, a regexp, or an array of them; string patterns are matched against the absolute module id, query included.

vite.config.js
srcset({
// Only the images of this directory, and none of the icons.
include: ['**/src/images/**/*.{jpg,jpeg,png}'],
exclude: [/\/node_modules\//, '**/src/icons/**']
})

Plugin options, all optional:

OptionTypeDefaultDescription
rulesSrcSetRule[]one empty ruleRules to generate the variants. A rule in the import query replaces them for that import.
placeholderboolean | { width, format }offAdds the placeholder export - a tiny variant inlined as a data-url. Defaults to 16px wide webp; format is 'webp' or 'jpg'.
select{ id?, format?, width? }the source format at the source widthWhich variant the default export and src point at. width may be an absolute value or a multiplier less than or equal to 1. The import query takes precedence.
resourceId(width, requestedWidth, format) => string`${format}${width}`Formatter of the srcMap keys and of src.id.
cacheboolean | { dir, maxAge }trueDisk cache of the generated variants - see Caching.
includestring | RegExp | (string | RegExp)[]the image extensions aboveIds to process.
excludestring | RegExp | (string | RegExp)[]/\/node_modules\//Ids to skip.

The generator options below are inherited from the shared generation core. Set them on the plugin as the defaults for every image; all of them except concurrency can also be set per rule, where they override the plugin-level value:

OptionTypeDefaultDescription
processing{ jpg?, png?, webp?, avif?, gif? }{ jpg: { mozjpeg: true }, png: { palette: true } }Sharp output options per format. Merged per format, so overriding one option keeps the rest of the defaults.
optimization{ jpg?, png?, webp?, avif?, gif?, svg? }noneCustom optimizer function per format, (contents, format) => Buffer, applied to the encoded contents. The only way to touch svg, e.g. with svgo.
skipOptimizationbooleanfalseDo not re-encode the original variant and skip the custom optimizers.
scalingUpbooleantrueKeep variants requested wider than the source, capped to the source width. false drops them instead. Pixels are never upscaled either way.
postfixstring | (width, requestedWidth, format) => string@<width>w, empty for the 1 multiplierPostfix added to the variant file name. A formatter must be pure - it also participates in the cache addressing.
concurrencynumberavailableParallelism()How many variants are encoded in parallel. Plugin level only.

The cache is on by default and lives in srcset/ inside the Vite cache directory - node_modules/.vite/srcset in a default setup. A cached variant is read from disk instead of being encoded again, so a repeated build only pays for the images that changed.

vite.config.js
srcset({
cache: {
dir: '.cache/srcset',
maxAge: 7 * 24 * 60 * 60 * 1000
}
})
  • dir - where to store the variants and their manifests.
  • maxAge - how long an unused entry survives, in milliseconds. Defaults to 30 days. Stale entries are pruned after the bundle is written, never before it: pruning first would drop the entries the build is about to hit.

The address of an entry covers the source contents, the variant and every generation option, function options included - they are keyed by their source text. Changing a rule, a sharp option or an optimizer function therefore misses the cache instead of serving a stale file.

The plugin ships ambient declarations for the image imports. Reference them once, in a .d.ts of the project or through the tsconfig types array:

src/vite-env.d.ts
/// <reference types="vite/client" />
/// <reference types="@srcset/vite-plugin/client" />

The srcset declarations cover only the named exports - src, srcSet, srcMap, placeholder. The default url export comes from the vite/client asset types, which @srcset/vite-plugin/client references itself, so the default import stays typed either way - keep the project’s own vite/client reference for the rest of the Vite types. A missing srcset reference shows up as Module '"*.jpg"' has no exported member 'srcSet'.

The query of a single import overrides the configured options for that import only:

import wide from './photo.jpg?{"width":[1,0.5],"format":["webp","jpg"]}'
import small from './photo.jpg?format=webp&width=600'

A JSON rule replaces the whole rule set; id=, format= and width= pick the variant behind the default export; placeholder and placeholder=false switch that export on and off. Parts combine with &. The full syntax is on the Image modules page.

  • No catch-all rule. An image matched by no rule produces an empty module - default export '', src is null, srcSet is [] - and the page silently renders nothing. Keep a rule without match last.
  • include is a replacement, not an addition. See the warning above.
  • A missing @srcset/vite-plugin/client reference. The image import then carries the default url export from vite/client and nothing else, and every named import fails to type-check.
  • Encoding is not free. The first build of an image-heavy project spends real time in sharp, and every build after it reads the variants back from the cache. On CI that cache sits in node_modules/.vite, so it survives only if the workflow restores that directory.