JORVEL

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.

tsx
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

The {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

PropTypePurpose
srcstringURL template — supports {w} token or appends ?w=N.
altstringRequired. Empty string for decorative images.
width / heightnumberIntrinsic dimensions. Always set both to prevent CLS.
widthsnumber[]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.

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

apps/shell/src/template.tsx
tsx
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.

bash
jorvel image \
  --app shell \
  --formats webp,avif \
  --widths 320,640,1024,1280,1920 \
  --quality 80
FlagDefaultNotes
--formatswebp,avifComma-separated. avif is smaller but slower to encode.
--widths320,640,1024,1280,1920Candidate widths. Match the widths prop on <Image>.
--quality801-100. 80 is usually invisible vs. lossless.
--inputpublic/imgSource directory scanned recursively.
--outputsame as inputDerivatives written next to originals by default.
--concurrencycpus()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:

ProviderQuery 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 workerImplement ?w=N&fmt=webp contract

Always set width and height

Missing intrinsic dimensions is the leading cause of CLS. The component prints a warning in dev if you forget. For unknown-size images (user uploads), persist the dimensions at upload time.