> ## Documentation Index
> Fetch the complete documentation index at: https://docs.pylonsync.com/llms.txt
> Use this file to discover all available pages before exploring further.

# <Image>

> Optimized images with built-in Rust resizer + mozjpeg + libwebp. Same binary, no Sharp / libvips install.

```tsx theme={null}
import { Image } from "@pylonsync/react";

<Image
  src="/hero.jpg"
  alt="Mountain at dawn"
  width={1200}
  height={800}
/>
```

`<Image>` renders a plain `<img>` pointing at Pylon's built-in optimizer endpoint. The browser fetches an AVIF or WebP (or JPEG, depending on `Accept`) sized for the user's viewport, served from disk cache on every subsequent request.

The pipeline is pure Rust: `image` for decode, `fast_image_resize` (SIMD) for the actual resize, and `ravif` / `rav1e` (AVIF), `libwebp`, and `mozjpeg` for encode. No Sharp install, no libvips on the host, no Node child-process. It all lives in the Pylon binary.

## What it does

For a single `<Image src="/hero.jpg" width={1200} height={800} />`:

* Renders `<img srcset>` with multiple width candidates (default: 1x and 2x of `width`, capped at 3840px).
* Each candidate points at `/_pylon/image?src=/hero.jpg&w=<width>&q=<quality>`.
* Browser picks the smallest candidate that satisfies the viewport DPR.
* Server decodes once, resizes with SIMD (Lanczos3), encodes as AVIF, WebP, or JPEG depending on `Accept`.
* Result is hashed by (src, width, quality, format) and cached at `.pylon/.cache/images/<hash>.<ext>`. Cache hits skip decode + encode entirely — just a file serve.
* Response carries `Cache-Control: public, max-age=31536000, immutable`.

## API

```tsx theme={null}
interface ImageProps {
  /** Source — site-relative ("/foo.jpg") or http(s) URL (allowlisted via env). */
  src: string;
  /** Intrinsic width in CSS px. Used for aspect ratio + the 1x srcset candidate. */
  width: number;
  /** Intrinsic height in CSS px. */
  height: number;
  /** Required alt text. Pass "" for purely decorative images. */
  alt: string;
  /** JPEG/WebP quality 1..=100. Default 75. PNG ignores it. */
  quality?: number;
  /** Override srcset widths. Default: [width, width*2] capped at 3840. */
  widths?: number[];
  /** `<img sizes>`. Default "100vw" — tighten for grid layouts. */
  sizes?: string;
  /** Skip lazy-loading, bump fetchPriority to "high". Use for above-the-fold heroes. */
  priority?: boolean;
  /** Plus any other <img> attribute (className, style, onClick, etc.). */
}
```

## Examples

### Hero image (above the fold)

```tsx theme={null}
<Image
  src="/hero.jpg"
  alt="Mountain at dawn"
  width={1920}
  height={1080}
  priority
  sizes="100vw"
  className="w-full h-[60vh] object-cover"
/>
```

`priority` skips `loading="lazy"` and sets `fetchPriority="high"` so the browser prioritizes it during the initial paint.

### Responsive grid

```tsx theme={null}
<ul className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4">
  {photos.map((p) => (
    <li key={p.id}>
      <Image
        src={p.src}
        alt={p.caption}
        width={600}
        height={400}
        sizes="(max-width: 640px) 100vw, (max-width: 1024px) 50vw, 33vw"
        className="aspect-[3/2] w-full object-cover rounded-2xl"
      />
    </li>
  ))}
</ul>
```

The `sizes` attribute is the most important knob — it tells the browser how wide the image will actually render at each breakpoint, so it picks the smallest viable srcset candidate. Without it, the browser assumes `100vw` and downloads a 4K image for a 200px thumbnail.

### Custom srcset

```tsx theme={null}
<Image
  src="/avatar.png"
  alt={user.name}
  width={64}
  height={64}
  widths={[64, 96, 128]}
  quality={85}
/>
```

Useful for avatar grids where you know the exact DPR multipliers you care about.

## Safety & configuration

The optimizer is opinionated about what it'll process. Every knob has a safe default, and you tune them via env. The same defaults apply across `pylon dev` and production.

### Allowed widths

The server rejects requests whose `w` isn't in its width set. This prevents cache-fill DoS where an attacker requests thousands of slightly-different widths to balloon your disk.

```bash theme={null}
# Defaults (mirror Next.js):
PYLON_IMAGE_DEVICE_SIZES=640,750,828,1080,1200,1920,2048,3840
PYLON_IMAGE_IMAGE_SIZES=16,32,48,64,96,128,256,384
```

`<Image>` generates srcset URLs only with widths from this combined set, so default-config usage just works. If you customize the env, also pass `widths={[...]}` on each `<Image>` to keep srcset URLs in range.

### Allowed qualities

Same defense, applied to the `q` parameter.

```bash theme={null}
PYLON_IMAGE_QUALITIES=50,75,90   # default
```

If you need finer-grained quality control, append it: `PYLON_IMAGE_QUALITIES=50,65,75,85,90,95`.

### Allowed formats

```bash theme={null}
PYLON_IMAGE_FORMATS=avif,webp,jpeg   # default
```

Locks the optimizer to a specific output set. The server picks the best match for the request's `Accept` header from this list. `<Image>` lets the server pick by default; pass `format=` in your own URLs if you want explicit control.

### Remote source allowlist

```bash theme={null}
PYLON_IMAGE_REMOTE_ALLOWLIST=cdn.example.com,images.unsplash.com/photos/
```

Each entry is `host` or `host/pathPrefix`:

| Pattern                  | Matches                                                           |
| ------------------------ | ----------------------------------------------------------------- |
| `cdn.example.com`        | any path on this exact host                                       |
| `cdn.example.com/users/` | only paths starting with `/users/`                                |
| `*.example.com`          | one-level wildcard — `foo.example.com`, not `foo.bar.example.com` |

Without this env, any `src` starting with `http://` or `https://` returns 400. This prevents SSRF — random visitors can't make your server fetch arbitrary internal URLs.

### Source size cap

```bash theme={null}
PYLON_IMAGE_MAX_BYTES=26214400   # 25MB default
```

Applies to both local files and remote fetches. Remote responses that exceed it are rejected mid-stream.

### Pixel-bomb protection

```bash theme={null}
PYLON_IMAGE_MAX_PIXELS=40000000   # 40M px (~24MP camera photo) default
```

A tiny PNG can declare a 100000×100000 canvas. Decoding that would allocate 40GB of RGBA. Pylon reads the image header BEFORE decoding and rejects any source whose declared `width × height` exceeds this cap. The default (40M px, \~160MB peak decode) is tuned so a burst of concurrent requests can't OOM a small machine; operators serving genuinely huge sources raise it explicitly.

### Fetch timeout

```bash theme={null}
PYLON_IMAGE_FETCH_TIMEOUT_MS=15000   # 15s default
```

Applies to remote source fetches.

### SVG handling

SVG is **always rejected**, both as input and output. SVG can carry inline `<script>` and CSS — letting users serve arbitrary SVG through an optimization endpoint would be an XSS vector. There's no `dangerouslyAllowSVG` toggle by design: if you need SVGs, render them with plain `<img src="/icon.svg">` (not through `<Image>`), serve them with `Content-Security-Policy: script-src 'none'`, and you're fine.

For embedded `<Image unoptimized>` use, see below.

### Local file source

`src` starting with `/` resolves under the frontend dir (`PYLON_FRONTEND_DIR` or `<app>/web/dist`). Path traversal is hard-rejected — canonicalize + prefix-check, so `..` segments and symlinks pointing outside the dir return 400.

### Bypassing the optimizer per-image

```tsx theme={null}
<Image src="/icon.svg" alt="" width={32} height={32} unoptimized />
```

Renders a plain `<img src="/icon.svg">` with no srcset and no optimizer round-trip. Useful for SVGs, animated GIFs, or images where the source is already pre-sized.

## Format selection

| Request `format=` query | Behavior                                                                                                                                                        |
| ----------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `avif`                  | Always AVIF, lossy (ravif / rav1e).                                                                                                                             |
| `webp`                  | Always WebP, lossy (libwebp).                                                                                                                                   |
| `jpeg` / `jpg`          | Always JPEG (mozjpeg).                                                                                                                                          |
| `png`                   | Always PNG (lossless).                                                                                                                                          |
| `(omitted)`             | AVIF when `Accept` explicitly advertises `image/avif`; otherwise WebP when the client accepts it or sends no `Accept` (the universal fallback); otherwise JPEG. |

AVIF is served automatically to browsers that advertise `image/avif` (Chrome, Firefox, Safari 16+) — it's the smallest of the three formats. The tradeoff is a slower cold encode (`rav1e` at speed 8), which the on-disk cache amortizes to a one-time cost per (src, width, quality) tuple. Browsers that don't advertise AVIF fall back to WebP, still \~30% smaller than mozjpeg at the same perceptual quality; unknown clients (curl, bots that send no `Accept`) also get WebP.

## Performance

Benchmarks on a 2400×1600 source JPEG (\~250KB) on Apple Silicon:

| Operation                                                | Time                            |
| -------------------------------------------------------- | ------------------------------- |
| Cold: decode + Lanczos3 resize to 1200×800 + WebP encode | 46–74ms                         |
| Cold: same path + JPEG (mozjpeg) encode                  | 50–80ms                         |
| Hot: cache hit, served from disk                         | 9ms (file serve only)           |
| WebP output size @ q=75                                  | 35KB (7.7× smaller than source) |
| JPEG output size @ q=75                                  | 51KB (5.4× smaller than source) |

Tail latency is bounded by the encoder; cache hits are bounded by your disk. For high-traffic surfaces, put a CDN in front and Pylon never re-processes anything — the `immutable` URL takes care of the rest.

## Env reference

| Env                            | Default                                | Purpose                                                                           |
| ------------------------------ | -------------------------------------- | --------------------------------------------------------------------------------- |
| `PYLON_FRONTEND_DIR`           | `<app>/web/dist`                       | Source root for local (`/path`) images.                                           |
| `PYLON_IMAGE_REMOTE_ALLOWLIST` | (empty)                                | `host` or `host/pathPrefix` entries for `http(s)://` sources. Empty = local only. |
| `PYLON_IMAGE_DEVICE_SIZES`     | `640,750,828,1080,1200,1920,2048,3840` | Allowed widths (full-bleed).                                                      |
| `PYLON_IMAGE_IMAGE_SIZES`      | `16,32,48,64,96,128,256,384`           | Allowed widths (thumbnails).                                                      |
| `PYLON_IMAGE_QUALITIES`        | `50,75,90`                             | Allowed quality values.                                                           |
| `PYLON_IMAGE_FORMATS`          | `avif,webp,jpeg`                       | Allowed output formats.                                                           |
| `PYLON_IMAGE_MAX_BYTES`        | `26214400` (25MB)                      | Source byte cap.                                                                  |
| `PYLON_IMAGE_MAX_PIXELS`       | `40000000` (40M, \~24MP)               | Decoded pixel cap. Pixel-bomb protection.                                         |
| `PYLON_IMAGE_FETCH_TIMEOUT_MS` | `15000`                                | Remote-source fetch timeout.                                                      |

The cache lives at `<cwd>/.pylon/.cache/images/`. Delete it to force regeneration. There's no LRU eviction yet — content-addressed entries are append-only, so the cache grows with the variety of (src, width, quality, format) tuples you serve. For production this is rarely a problem; the entire long-tail of a site's images is usually \< 1GB. A `pylon cache clean` command will arrive when it actually matters.

## Differences from Next.js's `<Image>`

|                   | Pylon `<Image>`                                        | Next.js `<Image>`                                           |
| ----------------- | ------------------------------------------------------ | ----------------------------------------------------------- |
| Backend           | Built-in Rust pipeline                                 | Sharp (libvips, separate npm install)                       |
| Formats           | AVIF, WebP, JPEG, PNG                                  | AVIF, WebP, JPEG, PNG                                       |
| Sources           | Local + remote with host + path-prefix patterns        | Local + remote with `images.remotePatterns`                 |
| Width allowlist   | `PYLON_IMAGE_DEVICE_SIZES` + `PYLON_IMAGE_IMAGE_SIZES` | `deviceSizes` + `imageSizes`                                |
| Quality allowlist | `PYLON_IMAGE_QUALITIES`                                | `qualities`                                                 |
| Format allowlist  | `PYLON_IMAGE_FORMATS`                                  | `formats`                                                   |
| Source size cap   | `PYLON_IMAGE_MAX_BYTES` (25MB default)                 | `contentLengthLimit`                                        |
| Pixel-bomb cap    | `PYLON_IMAGE_MAX_PIXELS` (100M default)                | implicit (Sharp's `limitInputPixels`)                       |
| SVG               | always rejected (no toggle)                            | `dangerouslyAllowSVG` opt-in                                |
| Per-image bypass  | `<Image unoptimized />`                                | `<Image unoptimized />`                                     |
| Placeholder       | Pass a CSS color or data URI yourself                  | Built-in `placeholder="blur"` with auto-generated blur data |
| Static import     | Not yet                                                | Yes (resolves intrinsic dims at compile time)               |

`placeholder="blur"` is on the roadmap. SVG support is deliberately off the roadmap — render SVG with a plain `<img>` tag, not through `<Image>`.
