Skip to main content
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 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 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 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

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)

<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

<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

<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.
# 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.
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

PYLON_IMAGE_FORMATS=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

PYLON_IMAGE_REMOTE_ALLOWLIST=cdn.example.com,images.unsplash.com/photos/
Each entry is host or host/pathPrefix:
PatternMatches
cdn.example.comany path on this exact host
cdn.example.com/users/only paths starting with /users/
*.example.comone-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

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

PYLON_IMAGE_MAX_PIXELS=100000000   # 100M (≈10000×10000) 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.

Fetch timeout

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

<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= queryBehavior
webpAlways WebP, lossy (libwebp).
jpeg / jpgAlways JPEG (mozjpeg).
pngAlways PNG (lossless).
(omitted)WebP if Accept advertises it (every modern browser), else JPEG.
WebP is the default because it produces ~30% smaller files than mozjpeg at the same perceptual quality. AVIF is not yet supported — the encoders (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:
OperationTime
Cold: decode + Lanczos3 resize to 1200×800 + WebP encode46–74ms
Cold: same path + JPEG (mozjpeg) encode50–80ms
Hot: cache hit, served from disk9ms (file serve only)
WebP output size @ q=7535KB (7.7× smaller than source)
JPEG output size @ q=7551KB (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

EnvDefaultPurpose
PYLON_FRONTEND_DIR<app>/web/distSource 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_SIZES640,750,828,1080,1200,1920,2048,3840Allowed widths (full-bleed).
PYLON_IMAGE_IMAGE_SIZES16,32,48,64,96,128,256,384Allowed widths (thumbnails).
PYLON_IMAGE_QUALITIES50,75,90Allowed quality values.
PYLON_IMAGE_FORMATSwebp,jpegAllowed output formats.
PYLON_IMAGE_MAX_BYTES26214400 (25MB)Source byte cap.
PYLON_IMAGE_MAX_PIXELS100000000 (≈10000×10000)Decoded pixel cap. Pixel-bomb protection.
PYLON_IMAGE_FETCH_TIMEOUT_MS15000Remote-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>
BackendBuilt-in Rust pipelineSharp (libvips, separate npm install)
FormatsWebP, JPEG, PNGAVIF, WebP, JPEG, PNG
SourcesLocal + remote with host + path-prefix patternsLocal + remote with images.remotePatterns
Width allowlistPYLON_IMAGE_DEVICE_SIZES + PYLON_IMAGE_IMAGE_SIZESdeviceSizes + imageSizes
Quality allowlistPYLON_IMAGE_QUALITIESqualities
Format allowlistPYLON_IMAGE_FORMATSformats
Source size capPYLON_IMAGE_MAX_BYTES (25MB default)contentLengthLimit
Pixel-bomb capPYLON_IMAGE_MAX_PIXELS (100M default)implicit (Sharp’s limitInputPixels)
SVGalways rejected (no toggle)dangerouslyAllowSVG opt-in
Per-image bypass<Image unoptimized /><Image unoptimized />
PlaceholderPass a CSS color or data URI yourselfBuilt-in placeholder="blur" with auto-generated blur data
Static importNot yetYes (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>.