<Image> renders a plain <img> pointing at Pylon’s built-in optimizer endpoint. The browser fetches a 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, mozjpeg and libwebp (both bundled via cc-rs) 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 ofwidth, 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 with mozjpeg or libwebp 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
Examples
Hero image (above the fold)
priority skips loading="lazy" and sets fetchPriority="high" so the browser prioritizes it during the initial paint.
Responsive grid
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
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 acrosspylon dev and production.
Allowed widths
The server rejects requests whosew isn’t in its width set. This prevents cache-fill DoS where an attacker requests thousands of slightly-different widths to balloon your disk.
<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 theq parameter.
PYLON_IMAGE_QUALITIES=50,65,75,85,90,95.
Allowed formats
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
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 |
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
Pixel-bomb protection
width × height exceeds this cap.
Fetch timeout
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
<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 |
|---|---|
webp | Always WebP, lossy (libwebp). |
jpeg / jpg | Always JPEG (mozjpeg). |
png | Always PNG (lossless). |
(omitted) | WebP if Accept advertises it (every modern browser), else JPEG. |
ravif + rav1e) would add ~10MB to the binary and 5-20x slower encodes. We may add it behind a feature flag once Pylon Cloud demonstrates the bandwidth win is worth the latency cost.
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) |
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 | webp,jpeg | Allowed output formats. |
PYLON_IMAGE_MAX_BYTES | 26214400 (25MB) | Source byte cap. |
PYLON_IMAGE_MAX_PIXELS | 100000000 (≈10000×10000) | Decoded pixel cap. Pixel-bomb protection. |
PYLON_IMAGE_FETCH_TIMEOUT_MS | 15000 | Remote-source fetch timeout. |
<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 | 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" and AVIF are on the roadmap. SVG support is deliberately off the roadmap — render SVG with a plain <img> tag, not through <Image>.