Image optimization
Images are the single biggest payload on most pages. @jorvel/runtime ships an <Image> component plus pure helpers (buildSrcset, buildSizes, buildImagePreloadLink) that emit the right srcset / sizes attributes. Pair them with the jorvel image CLI to actually produce WebP / AVIF derivatives in CI.
The Image component
Drop in for a raw <img>. Renders a <picture> with one<source>per modern format, falls back to the legacy URL on browsers that don't support the modern formats. Always sets width / height attributes so the browser reserves layout space — zero CLS.
import { Image } from '@jorvel/runtime';
<Image
src="/img/hero-{w}.jpg"
alt="Hero"
width={1600}
height={900}
widths={[640, 1024, 1600, 1920]}
formats={['avif', 'webp']}
breakpoints={[
{ minWidth: 1280, size: '50vw' },
{ minWidth: 768, size: '70vw' },
]}
/>Token rewriting
{w} token is replaced with each width. If the URL has no token, the helper appends ?w=<n> so CDN-side imagers (Vercel, Netlify, Cloudflare Image Resizing, your own CF worker) can resize on the fly.Props
| Prop | Type | Purpose |
|---|---|---|
src | string | URL template — supports {w} token or appends ?w=N. |
alt | string | Required. Empty string for decorative images. |
width / height | number | Intrinsic dimensions. Always set both to prevent CLS. |
widths | number[] | Candidate widths for the srcset. |
formats | ('avif'|'webp'|'jpg')[] | Emitted in order — first match wins. |
breakpoints | { minWidth, size }[] | Drives the sizes attribute. |
fetchPriority | 'high' | 'low' | 'auto' | Set 'high' on the LCP image. |
loading | 'lazy' | 'eager' | Defaults to 'lazy' for everything below the fold. |
Standalone helpers
Sometimes you want the srcset string for a CSS background-image or a native<img>. The pure helpers return strings and JSON.
import { buildSrcset, buildSizes, buildImagePreloadLink } from '@jorvel/runtime';
buildSrcset('/img/hero-{w}.webp', { widths: [320, 640, 1280] });
// '/img/hero-320.webp 320w, /img/hero-640.webp 640w, /img/hero-1280.webp 1280w'
buildSrcset('/img/hero-{w}.webp', { density: [1, 2, 3] });
// '/img/hero-1000.webp 1x, /img/hero-2000.webp 2x, /img/hero-3000.webp 3x'
buildSizes({
breakpoints: [{ minWidth: 1280, size: '33vw' }, { minWidth: 768, size: '50vw' }],
fallback: '100vw',
});
// '(min-width: 1280px) 33vw, (min-width: 768px) 50vw, 100vw'
const link = buildImagePreloadLink('/img/lcp-{w}.webp', { widths: [640, 1280], fetchPriority: 'high' });
// <link rel="preload" as="image" imagesrcset="..." imagesizes="..." fetchpriority="high">LCP — preloading the hero image
The Largest Contentful Paint is usually the hero. Preload it in the document head so the browser fetches it in parallel with the HTML. Use the same sizes attribute on the preload and the <img> so the browser picks the same candidate from the srcset.
import { buildImagePreloadLink } from '@jorvel/runtime';
const heroPreload = buildImagePreloadLink('/img/hero-{w}.webp', {
widths: [640, 1024, 1280, 1920],
sizes: '(min-width: 1280px) 50vw, 100vw',
fetchPriority: 'high',
});
export function Head() {
return (
<head>
<link {...heroPreload} />
</head>
);
}Generating derivatives
Run the CLI in CI to produce WebP / AVIF copies at every target width. Output filenames follow the pattern the component expects (name-<width>.webp). Re-runs are cached on a content-hash basis so unchanged sources skip work.
jorvel image \
--app shell \
--formats webp,avif \
--widths 320,640,1024,1280,1920 \
--quality 80| Flag | Default | Notes |
|---|---|---|
--formats | webp,avif | Comma-separated. avif is smaller but slower to encode. |
--widths | 320,640,1024,1280,1920 | Candidate widths. Match the widths prop on <Image>. |
--quality | 80 | 1-100. 80 is usually invisible vs. lossless. |
--input | public/img | Source directory scanned recursively. |
--output | same as input | Derivatives written next to originals by default. |
--concurrency | cpus() | Cap when CI memory is tight. |
CDN-side resizing
For user-generated images, pre-generating every size is impractical. Set up your CDN imager to honor a ?w=N query — the Image component picks that up automatically. Examples:
| Provider | Query syntax |
|---|---|
| Vercel | /_vercel/image?url=...&w=640&q=80 (use the wrapper imager) |
| Cloudflare Image Resizing | /cdn-cgi/image/width=640/... |
| imgix | ?w=640&fm=webp |
| Custom worker | Implement ?w=N&fmt=webp contract |
Always set width and height
