Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 32 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ jobs:
- 'pnpm-workspace.yaml'
- 'package.json'
- 'tsconfig.json'
- 'tsconfig.bun.json'
- 'vitest.config.mts'
- 'build.config.ts'
- 'eslint.config.mjs'
Expand Down Expand Up @@ -123,20 +124,51 @@ jobs:
env:
CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}

bun:
needs: changes
if: needs.changes.outputs.src == 'true'
strategy:
matrix:
# macOS covers the OS codec path (HEIC/AVIF)
os: [ubuntu-latest, macos-latest]
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- run: corepack enable
- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
with:
node-version: lts/-1
cache: "pnpm"
- uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2.2.0
with:
bun-version: 1.4.x

- name: πŸ“¦ Install dependencies
run: pnpm install

- run: pnpm dev:prepare

- name: πŸ₯Ÿ Test bun provider (Bun.Image)
run: pnpm test:bun

ci-ok:
if: always()
runs-on: ubuntu-slim
needs:
- changes
- lint
- ci
- bun
steps:
- name: Check required jobs
run: |
results=( \
"${{ needs.changes.result }}" \
"${{ needs.lint.result }}" \
"${{ needs.ci.result }}" \
"${{ needs.bun.result }}" \
)
for result in "${results[@]}"; do
if [[ "$result" == "failure" || "$result" == "cancelled" ]]; then
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ Plug-and-play image optimization for Nuxt apps. Resize and transform your images

- `<nuxt-img>` drop-in replacement for the native `<img>` element
- `<nuxt-picture>` drop-in replacement for the native `<picture>` element.
- Built-in image resizer and transformer with [unjs/ipx](https://github.com/unjs/ipx)
- Built-in image resizer and transformer with [unjs/ipx](https://github.com/unjs/ipx), or with [Bun.Image](https://bun.com/docs/runtime/image) and zero native dependencies when running on Bun
- Support 20+ providers
- Generate responsive sizes
- Optimize using modern formats such as webp and avif
Expand Down
4 changes: 4 additions & 0 deletions docs/content/1.get-started/1.installation.md
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,10 @@ export default defineNuxtConfig({

## Sharp Cross-Platform Installation

::note
Deploying on Bun? The [bun provider](/providers/bun) uses `Bun.Image` instead of sharp and needs no native binaries at all.
::

The default [IPX](/providers/ipx) provider uses [sharp](https://sharp.pixelplumbing.com) for image processing. At install time, your package manager downloads prebuilt sharp binaries **only for your current OS and CPU architecture**. This means if you build on macOS or Windows and deploy to a Linux server (or build on x64 and deploy to ARM64), the required sharp binaries might be missing, resulting in errors like:

```
Expand Down
10 changes: 5 additions & 5 deletions docs/content/1.get-started/2.configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -155,7 +155,7 @@ export default defineNuxtConfig({

## `provider`

Default: `ipx` (or `ipxStatic` if used with a static nitro preset, such as if you are running `nuxt generate`)
Default: `auto`, which resolves to a self-hosted engine: [`bun`](/providers/bun) when the build runs on Bun or targets the `bun` Nitro preset, otherwise [`ipx`](/providers/ipx) when that package is installed (or `ipxStatic` / `bunStatic` with a static nitro preset, such as when running `nuxt generate`)

We can specify default provider to be used when not specified in component or when calling `$img`.

Expand Down Expand Up @@ -209,7 +209,7 @@ export default defineNuxtConfig({

Default: `public`

This option allows you to specify the location of the source images when using the `ipx` or `ipxStatic` provider.
This option allows you to specify the location of the source images when using a self-hosted provider (`ipx`, `ipxStatic`, `bun` or `bunStatic`).

For example you might want the source images in `assets/images` directory rather than the default `public` directory so the source images don't get copied into `dist` and deployed:

Expand All @@ -223,15 +223,15 @@ export default defineNuxtConfig({

**Notes:**

- For `ipxStatic` provider, if images weren't crawled during generation (unreachable modals, pages or dynamic runtime size), changing `dir` from `public` causes 404 errors.
- For `ipx` provider, make sure to deploy customized `dir` as well.
- For `ipxStatic` and `bunStatic` providers, if images weren't crawled during generation (unreachable modals, pages or dynamic runtime size), changing `dir` from `public` causes 404 errors.
- For `ipx` and `bun` providers, make sure to deploy customized `dir` as well.
- For some providers (like vercel), using a directory other than `public/` for assets is not supported since resizing happens at runtime (instead of build/generate time) and source fetched from the `public/` directory (deployment URL)

## `alias`

This option allows you to specify aliases for `src`.

When using the default ipx provider, URL aliases are shortened on the server-side.
When using a self-hosted provider (`ipx` or `bun`), URL aliases are shortened on the server-side.
This is especially useful for optimizing external URLs and not including them in HTML.

When using other providers, aliases are resolved in runtime and included in HTML. (only the usage is simplified)
Expand Down
2 changes: 1 addition & 1 deletion docs/content/1.get-started/3.providers.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ Nuxt Image will automatically optimize `<NuxtImg>` and `<NuxtPicture>` sources a

## Default Provider

The default optimizer and provider for Nuxt Image is [ipx](/providers/ipx). Either option can be used without any configuration.
Nuxt Image ships two self-hosted optimizers that work without any configuration: [ipx](/providers/ipx), based on sharp, and [bun](/providers/bun), based on `Bun.Image`. With `provider: 'auto'` the module uses `bun` when the build runs on Bun or targets the `bun` Nitro preset, and `ipx` when that package is installed.

### Local Images

Expand Down
182 changes: 182 additions & 0 deletions docs/content/3.providers/bun.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,182 @@
---
title: Bun
description: Self-hosted image optimizer powered by Bun.Image, with no native dependencies.
links:
- label: Source
icon: i-simple-icons-github
to: https://github.com/nuxt/image/blob/main/src/runtime/providers/bun.ts
size: xs
---

The `bun` provider transforms images with [`Bun.Image`](https://bun.com/docs/runtime/image), the image pipeline built into the Bun runtime. It is the self-hosted alternative to [ipx](/providers/ipx): the same URLs and modifier names, but no `sharp`, no native binaries and no `@img/*` packages to install or trace into your build. In [benchmarks](#performance) it also resizes JPEG photos 1.2Γ— to 1.9Γ— faster than ipx, serves about 40% more requests per second under load and uses less than half the memory, at the cost of somewhat larger output files.

::warning
The `bun` provider needs **Bun 1.4.0 or newer** wherever images are transformed: the dev server, the production server, and the build process when you prerender. See [Running on Bun](#running-on-bun).
::

## Support matrix

Currently, `Bun.Image` implements a subset of what sharp offers. Every modifier ipx accepts is listed below with its status on the `bun` provider. Modifiers marked ❌ or ⏳ are ignored and produce a console warning (see [Unsupported modifiers](#unsupported-modifiers)).

Legend: βœ… Works as with ipx Β· 🟑 Partial or emulated, read the note Β· ⏳ Waits on an open Bun pull request Β· ❌ Not available, a console warning is emitted.

| Status | Modifier | ipx / sharp | Bun.Image | Note |
| :---: | --- | --- | --- | --- |
| βœ… | `width`, `w` | resize width | resize width | |
| βœ… | `height`, `h` | resize height | resize height | Source size is read first. |
| βœ… | `resize`, `s` | resize WΓ—H | resize WΓ—H | Default fit differs, see fit. |
| βœ… | `enlarge` | allow upscale | withoutEnlargement: false | |
| βœ… | `kernel` | resampling kernel | filter | Same names plus mks2013, mks2021, box. |
| βœ… | `format`, `f` | jpeg, png, webp, avif, heif, gif, tiff | jpeg, png, webp, avif, heic | No gif or tiff output. avif and heic need OS codecs, none on Linux. Falls back with a warning. |
| βœ… | `quality`, `q` | 1-100 | 1-100 | Ignored for png. |
| βœ… | `flip`, `flop` | mirror | mirror | |
| βœ… | `grayscale` | desaturate | modulate({ saturation: 0 }) | |
| βœ… | `brightness`, `saturation` | modulate | modulate | |
| βœ… | `autoorient` | apply EXIF | on by default | |
| 🟑 | `fit` | contain, cover, fill, inside, outside | fill, inside | cover, contain and outside are emulated without cropping or padding unless the runtime supports them natively ([oven-sh/bun#30616](https://github.com/oven-sh/bun/pull/30616)). |
| 🟑 | `rotate` | any angle | multiples of 90 | Other angles return 400. |
| 🟑 | `modulate` | brightness, saturation, hue, lightness | brightness, saturation | hue and lightness positions are ignored. |
| 🟑 | `background`, `b` | fill colour for contain, extend, rotate, flatten | resize background | Only used with native contain ([oven-sh/bun#30616](https://github.com/oven-sh/bun/pull/30616)). |
| 🟑 | `animated`, `a` | keep frames | first frame only | |
| ⏳ | `extract`, `crop` | crop region | none | [oven-sh/bun#40379](https://github.com/oven-sh/bun/pull/40379). |
| ⏳ | `opacity` | composite overlay | none | Needs composite, [oven-sh/bun#31670](https://github.com/oven-sh/bun/pull/31670). |
| ⏳ | `tint` | tint | none | Needs raw pixel access, [oven-sh/bun#31670](https://github.com/oven-sh/bun/pull/31670). |
| ❌ | `position`, `pos` | crop gravity | none | Bun.Image has no crop gravity; cover is centre-crop only. |
| ❌ | `trim` | trim edges | none | Bun.Image has no trim operation. |
| ❌ | `extend` | pad canvas | none | Bun.Image has no extend operation. |
| ❌ | `sharpen` | unsharp mask | none | Bun.Image has no sharpen operation. |
| ❌ | `median` | median filter | none | Bun.Image has no median filter. |
| ❌ | `blur` | gaussian blur | none | Bun.Image has no blur operation. |
| ❌ | `dilate`, `erode` | morphology | none | Bun.Image has no morphology operations. |
| ❌ | `clahe` | contrast equalisation | none | Bun.Image has no CLAHE operation. |
| ❌ | `flatten`, `unflatten` | alpha handling | none | Bun.Image has no flatten or unflatten operation. |
| ❌ | `gamma` | gamma curve | none | Bun.Image has no gamma operation. |
| ❌ | `negate` | invert | none | Bun.Image has no negate operation. |
| ❌ | `normalize` | stretch histogram | none | Bun.Image has no normalize operation. |
| ❌ | `threshold` | binarise | none | Bun.Image has no threshold operation. |
| ❌ | `linear` | a·x + b | none | Bun.Image has no linear operation. |
| ❌ | `hue`, `lightness` | modulate channel | none | Bun.Image modulate has no hue or lightness. |

Bun-only encoder options are also accepted as modifiers: `progressive` (JPEG), `lossless` (WebP), `palette`, `colors`, `dither` and `compressionLevel` (PNG).

### Output formats by platform

| Format | Linux | macOS | Windows |
| --- | :---: | :---: | :---: |
| JPEG, PNG, WebP | βœ… | βœ… | βœ… |
| AVIF | ❌ falls back to WebP | βœ… decode, encode on Apple Silicon M3+ | βœ… with the AV1 Video Extension |
| HEIC | ❌ falls back to JPEG | βœ… | βœ… with the HEIF Image Extensions |
| GIF, TIFF | ❌ falls back to WebP / PNG | ❌ falls back | ❌ falls back |

When a format cannot be encoded on the current machine the provider logs one warning and serves the fallback. Keep the module default of `format: ['webp']` on Linux hosts; with `<NuxtPicture format="avif,webp">` the AVIF source would receive WebP bytes.

Sources `Bun.Image` cannot decode are served untouched with a warning: SVG, animated WebP, and AVIF, HEIC or TIFF on platforms without a codec. Animated GIFs are decoded to their first frame unless the `animated` modifier is set, in which case the file is served untouched.

## Unsupported modifiers

Whenever a `<NuxtImg>`, `<NuxtPicture>`, `useImage()` or `$img` call passes a modifier from the ❌ or ⏳ rows, the provider warns once per modifier name:

```
[@nuxt/image] The "blur" modifier is not supported by the bun provider (Bun.Image has no blur operation) and was ignored for "/images/hero.jpg". See https://image.nuxt.com/providers/bun#support-matrix
```

The warning fires where the URL is generated (during SSR and in the browser) and again on the server for hand-written URLs. The `unsupported` option controls it:

| Value | URL generation | Server |
| --- | --- | --- |
| `'warn'` (default) | `console.warn` once per modifier | log once per modifier, apply what it can |
| `'error'` | `console.warn` | respond `400` naming the modifier |
| `'silent'` | nothing | nothing |

### Fit modes

Bun 1.4 resizes with `fill` and `inside` only. `cover` (the default when both `width` and `height` are given, matching ipx), `outside` and `contain` are emulated: the image is scaled so it covers or fits the box, but it is neither cropped nor padded, so the output can be larger or smaller than the box on one axis. Use CSS `object-fit` on the `<img>` when the exact box matters, or set `defaultFit: 'inside'`. Native support is detected at startup, so once Bun ships the remaining fit modes the provider uses them without a change.

## Performance

`pnpm bench:image-engines` builds the playground twice from the same code, ipx served by Node and the bun provider served by Bun, and sends both production servers the same requests. Broadly, on the machine below the bun provider:

- transforms JPEG photos **1.2Γ— to 1.9Γ— faster** per request, with the largest gain on big outputs (a 1200 px wide JPEG from a 24 MP source: 250 ms with ipx, 140 ms with bun);
- handles a mixed concurrent workload with **about 40% more throughput** and a **p95 latency 35% to 45% lower**;
- serves a page with ten resized images **about 25% faster** (roughly 400 ms with ipx, 300 ms with bun, measured from the page request to the last image byte with six requests);
- runs in **less than half the memory**, about 350 MB of server RSS against 750 MB to 800 MB for ipx after the same workload;
- is **slower on PNG to PNG** re-encoding (about 14 ms against 11 ms) and produces **5% to 40% larger files** at the same nominal quality, most visibly on small WebP thumbnails, because the encoders and their defaults differ. Set `defaults` or a lower `quality` if bytes matter more than CPU time.

The SVG row is not a like-for-like comparison: ipx optimises SVG with svgo, the bun provider serves it untouched, which is why it is ten times faster and returns the original size.

| Request (median of 20, warm) | ipx (sharp) | bun (Bun.Image) |
| --- | ---: | ---: |
| 24 MP JPEG β†’ 300Γ—300 box, WebP | 157 ms Β· 21 KB | 125 ms Β· 30 KB |
| 24 MP JPEG β†’ 1200 px JPEG, q80 | 256 ms Β· 168 KB | 137 ms Β· 177 KB |
| 2 MP JPEG β†’ 640 px WebP | 101 ms Β· 115 KB | 87 ms Β· 119 KB |
| PNG logo β†’ 200 px WebP | 11 ms Β· 1.7 KB | 8 ms Β· 1.7 KB |
| PNG logo β†’ 400 px PNG | 11 ms Β· 14 KB | 15 ms Β· 16 KB |
| 64 mixed requests, 8 in flight | 32 req/s, p95 446 ms | 44 req/s, p95 335 ms |

Numbers move with hardware and versions; run the script on your own machine for the comparison that matters to you.

## Configuration

```ts [nuxt.config.ts]
export default defineNuxtConfig({
image: {
provider: 'bun', // or leave 'auto' and run on Bun
bun: {
baseURL: '/_bun',
maxAge: 60,
maxOutputDimension: 8192,
maxPixels: 268_435_456,
fs: { dir: 'public', maxAge: 60 },
http: { domains: ['images.unsplash.com'], maxAge: 300 },
unsupported: 'warn',
defaultFit: 'cover',
defaults: {
jpeg: { progressive: true },
webp: {},
png: {},
},
},
},
})
```

| Option | Default | Description |
| --- | --- | --- |
| `baseURL` | `/_bun` | Route the handler is mounted on. |
| `maxAge` | `60` | Default `cache-control` max-age in seconds. |
| `maxOutputDimension` | `8192` | Cap on output width and height, `false` to disable. |
| `maxPixels` | Bun default | Reject sources with more pixels, before decoding. |
| `fs` | `{ dir: <public dirs> }` | Local storage. `false` to disable. Accepts `maxAge` and `allowSymlinksOutsideDir`. |
| `http` | `{ domains: <image.domains> }` | Remote storage. `false` to disable. Accepts `allowAllDomains`, `maxAge`, `ignoreCacheControl`, `fetchOptions`. Credential headers in `fetchOptions` are only sent over HTTPS to the requested origin. |
| `unsupported` | `'warn'` | What to do with modifiers Bun.Image cannot apply. |
| `defaultFit` | `'cover'` | Fit used when both `width` and `height` are given without `fit`. |
| `defaults` | `{}` | Per-format encoder defaults. |

The option shape mirrors ipx, so switching engines is a one-line change. [Aliases](/get-started/configuration#alias) and [domains](/get-started/configuration#domains) apply to both.

## Running on Bun

`Bun.Image` only exists inside the Bun runtime, so:

- **Development**: start with `bun --bun nuxt dev`. Under Node the module warns at startup and image requests fail with a clear `500`.
- **Production**: build with `NITRO_PRESET=bun` (or `nitro.preset: 'bun'`) and run `bun .output/server/index.mjs`.
- **Static sites**: `nuxt generate` transforms images inside the build process, so run `bun --bun nuxt generate`. The provider is then `bunStatic`, the counterpart of `ipxStatic`.

With `provider: 'auto'` the module picks the engine whose options are configured (`image.bun` or `image.ipx`). When neither or both are configured it picks `bun` when the build runs on Bun or targets the `bun` preset, `ipx` when that package is installed, and warns otherwise.

## Runtime configuration

Options can be overridden at runtime through `runtimeConfig.bunImage`:

```ts [nuxt.config.ts]
export default defineNuxtConfig({
runtimeConfig: {
bunImage: {
baseURL: process.env.NUXT_BUN_IMAGE_BASE_URL || '/_bun',
http: {
domains: process.env.NUXT_BUN_IMAGE_HTTP_DOMAINS,
},
},
},
})
```
4 changes: 4 additions & 0 deletions docs/content/3.providers/ipx.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,10 @@ links:

Nuxt Image comes with a [preconfigured instance](/get-started/providers#default-provider) of [unjs/ipx](https://github.com/unjs/ipx) - an open source, self-hosted image optimizer based on [lovell/sharp](https://github.com/lovell/sharp).

::note
ipx is an optional dependency. If you deploy on Bun, the [bun provider](/providers/bun) offers the same URLs and modifiers with `Bun.Image` and no native binaries.
::

## Additional Modifiers

You can use [additional modifiers](https://github.com/unjs/ipx/#modifiers) supported by IPX.
Expand Down
2 changes: 2 additions & 0 deletions docs/content/4.advanced/2.static-images.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ description: Optimizing images for static websites.

If you are building a static site using `nuxt generate`, Nuxt Image will optimize and save your images locally when your site is generated - and deploy them alongside your generated pages.

This works with both self-hosted providers: `ipxStatic` (the default, files under `/_ipx/`) and `bunStatic` (files under `/_bun/`). The [bun provider](/providers/bun) transforms images inside the build process, so run `bun --bun nuxt generate` to use it.

If you disabled server-side rendering (`ssr: false` in the `nuxt.config`), Nuxt Image won't be able to optimize your images during the static generation process.

In that case, you can tell Nuxt to pre-render images by using the `nitro.prerender.routes` option:
Expand Down
6 changes: 4 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -41,8 +41,10 @@
"lint:fix": "eslint . --cache --fix",
"prepack": "nuxt-module-build prepare && pnpm build",
"test": "nuxt prepare playground && pnpm vitest run --coverage",
"test:types": "vue-tsc --noEmit",
"test:built:types": "nuxt typecheck playground && nuxt typecheck example"
"test:bun": "bun test --tsconfig-override tsconfig.bun.json test/bun",
"test:types": "vue-tsc --noEmit && tsc -p tsconfig.bun.json",
"test:built:types": "nuxt typecheck playground && nuxt typecheck example",
"bench:image-engines": "node scripts/bench-image-engines.ts"
},
"dependencies": {
"@noble/hashes": "^2.4.0",
Expand Down
Loading
Loading