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.