Image Optimization

bext optimizes images on-the-fly at the edge with zero build-time preprocessing. Request an image with query parameters and bext returns a resized, format-converted variant served from a tiered cache. Original files are never modified.

Configuration

[image]
enabled = true
quality = 80                    # Default JPEG/WebP quality (1-100)
max_width = 3840                # Reject requests above this width
allowed_formats = ["jpeg", "png", "webp"]
cache_dir = ".bext/image-cache" # On-disk variant cache
auto_format = true              # Negotiate WebP/AVIF from Accept header
blur_placeholder = true         # Enable LQIP generation

# Predefined srcset widths for the /_bext/srcset endpoint
srcset_widths = [320, 640, 960, 1280, 1920]

Query Parameter API

Append query parameters to any image URL served by bext:

/images/hero.jpg?w=800&format=webp&q=75
Parameter Description Default
w Target width in pixels (aspect ratio preserved) Original width
q Quality (1-100, applies to JPEG and WebP) 80
format Output format: jpeg, png, webp Auto-negotiated
blur Return a tiny blur placeholder (blur=1) false

Images are never upscaled. If w exceeds the original width, the original dimensions are preserved.

Examples

# Resize to 640px wide, convert to WebP at quality 75
curl https://example.com/photos/landscape.jpg?w=640&format=webp&q=75

# Get a blur placeholder (tiny base64-encoded JPEG, ~200 bytes)
curl https://example.com/photos/landscape.jpg?blur=1

Format Negotiation

When auto_format = true, bext reads the Accept header and automatically serves the best format the client supports:

Accept header contains Served format
image/webp WebP
Neither JPEG (fallback)

This means you can use a single <img> tag and let bext handle format selection:

<img src="/images/hero.jpg?w=800" alt="Hero image" />
<!-- Chrome/Firefox receive WebP; Safari receives JPEG -->

Blur Placeholders (LQIP)

Low-Quality Image Placeholders give instant visual feedback while the full image loads. bext generates an 8x8px Gaussian-blurred JPEG thumbnail encoded as a base64 data URI (typically under 200 bytes).

<!-- Use the blur placeholder as a CSS background -->
<div
  style="background-image: url('/images/hero.jpg?blur=1')"
  class="blur-placeholder"
>
  <img
    src="/images/hero.jpg?w=800"
    loading="lazy"
    alt="Hero"
    onload="this.parentElement.style.backgroundImage='none'"
  />
</div>

Or fetch it server-side during SSR:

// In your SSR data loader
const placeholder = await fetch("/images/hero.jpg?blur=1").then(r => r.text());
// Returns: "data:image/jpeg;base64,/9j/4AAQ..."

Srcset Generation

For responsive images, use the built-in srcset endpoint to get all variants in one request:

GET /_bext/srcset?src=/images/hero.jpg&format=webp

Returns a JSON object with pre-computed URLs for each configured width:

{
  "srcset": "/images/hero.jpg?w=320&format=webp 320w, /images/hero.jpg?w=640&format=webp 640w, /images/hero.jpg?w=960&format=webp 960w, /images/hero.jpg?w=1280&format=webp 1280w, /images/hero.jpg?w=1920&format=webp 1920w",
  "sizes": "(max-width: 640px) 100vw, (max-width: 1280px) 50vw, 33vw",
  "placeholder": "data:image/jpeg;base64,/9j/4AAQ..."
}

Use this in your templates:

<img
  srcset="/images/hero.jpg?w=320&format=webp 320w,
         /images/hero.jpg?w=640&format=webp 640w,
         /images/hero.jpg?w=1280&format=webp 1280w,
         /images/hero.jpg?w=1920&format=webp 1920w"
  sizes="(max-width: 640px) 100vw, 50vw"
  src="/images/hero.jpg?w=1280&format=webp"
  alt="Hero image"
  loading="lazy"
/>

Cache Behavior

Image variants are cached at two levels:

1. In-memory L1 -- recent variants are held in a DashMap (bounded by cache.isr.max_entries). Sub-millisecond response for hot images. 2. On-disk L2 -- all generated variants are persisted to cache_dir. Survives server restarts.

Cache keys are derived from a hash of the original URL combined with the width, quality, and format:

{xxhash64_hex}/{width}/q{quality}.{ext}
# Example: a1b2c3d4e5f67890/800/q75.webp

When the original file changes (detected by mtime or ETag), cached variants are automatically invalidated.

Resize Algorithm

bext uses the Lanczos3 resampling filter for high-quality downscaling. This is the same algorithm used by professional image editors -- it preserves sharp edges and fine detail better than bilinear or bicubic interpolation, at a small CPU cost that is amortized by caching.

Performance Tips

- Set explicit widths -- avoid serving the original 4000px image to mobile clients. A ?w=640 saves 80%+ bandwidth.

- Use WebP -- typically 25-35% smaller than JPEG at equivalent quality. Enable auto_format to serve it automatically.

- Pre-warm popular images -- use the task scheduler to call popular image URLs at startup, priming the L1 cache.

- Tune quality -- for hero images, q=80 is a good default. For thumbnails, q=60 is often indistinguishable and significantly smaller.