Image Optimization Lab
Tune next/image quality and sizes against a real optimized asset, plus format heuristics.
Run this experiment yourself
Demos are not embedded on this site. Deploy a standalone copy on Vercel or run the experiment app locally.
Local development
cd apps/experiments/image-optimization-lab pnpm install pnpm dev
Then open http://localhost:3010.
This is an experimental demo. Use it as a starting point for your own projects.
The Image Optimization Lab is a client-only experiment that pairs a live next/image preview with the responsive attributes you would otherwise write by hand: srcset, sizes, width, height, and quality. It is built with Next.js next/image, React state, and a small set of pure TypeScript helpers in apps/experiments/image-optimization-lab/logic.ts.
It teaches how display width, quality, and responsive hints relate to image transfer size, and why you should let the framework generate the real variants. Two parts of the page are different in kind, and the UI labels them accordingly:
- The preview is real. It renders
next/image, which uses Vercel Image Optimization in production. - The byte table and generated
srcsettext are teaching heuristics. They are calculated locally and are not measured CDN output.
Features
- Live
next/imagepreview withpriority,sizes, andqualitydriven by your inputs. - Display width slider (320–2400 px, step 40) and quality slider (40–100).
- Editable sample path (local paths beginning with
/). - Generated
srcSetandsizesattribute text. - Heuristic transfer-size table comparing AVIF, WebP, and JPEG at the chosen width and quality.
- Copyable
next/imagesnippet that follows your width and quality. - Reset button that restores width
1200, quality75, and/demo-hero.svg.
UI Reference
This experiment has no API routes. All controls are client-side.
Prop
Type
The only image shipped with the experiment is public/demo-hero.svg (1600 x 900).
Implementation Details
Flow
Render the real image
The preview passes your values straight to next/image. Width is capped at 1200 and height is derived from a 16:9 aspect ratio.
<Image
src={path.startsWith('/') ? path : '/demo-hero.svg'}
alt="Optimized demo hero"
width={Math.min(width, 1200)}
height={Math.round((Math.min(width, 1200) * 9) / 16)}
quality={quality}
sizes={sizes}
className="h-auto w-full object-cover"
priority
/>Build the sizes attribute
buildSizesAttribute sorts breakpoints from widest to narrowest and appends 100vw as the default. The demo uses two breakpoints.
export function buildSizesAttribute(breakpoints: { minWidth: number; size: string }[]): string {
const parts = breakpoints
.filter((b) => b.minWidth >= 0)
.sort((a, b) => b.minWidth - a.minWidth)
.map((b) => `(min-width: ${b.minWidth}px) ${b.size}`);
parts.push('100vw');
return parts.join(', ');
}For [{ minWidth: 1024, size: '50vw' }, { minWidth: 640, size: '75vw' }] the result is:
(min-width: 1024px) 50vw, (min-width: 640px) 75vw, 100vwGenerate illustrative srcset text
computeSrcSet produces a ?w= style list for the widths 640, 828, 1080, 1200, 1920.
export function computeSrcSet(basePath: string, widths: number[]): string {
const sorted = [...widths].filter((w) => w > 0).sort((a, b) => a - b);
return sorted.map((w) => `${basePath}?w=${w} ${w}w`).join(', ');
}/demo-hero.svg?w=640 640w, /demo-hero.svg?w=828 828w, /demo-hero.svg?w=1080 1080w, /demo-hero.svg?w=1200 1200w, /demo-hero.svg?w=1920 1920wThis is a display of the pattern, not the URL list Next.js actually emits. next/image generates its own srcset that points at the /_next/image endpoint.
Estimate bytes by format
estimateBytes multiplies pixel count by a per-format factor and by quality.
const FORMAT_FACTOR: Record<ImageFormat, number> = {
avif: 0.07,
webp: 0.11,
jpeg: 0.22,
};
export function estimateBytes(
width: number,
format: ImageFormat,
quality: number,
aspectRatio = 16 / 9,
): number {
const w = Math.max(1, Math.floor(width));
const h = Math.max(1, Math.floor(w / aspectRatio));
const q = Math.min(100, Math.max(1, quality)) / 100;
const pixels = w * h;
return Math.round(pixels * FORMAT_FACTOR[format] * q);
}At the defaults (1200 px, quality 75) the table shows 42,525 B for AVIF, 66,825 B for WebP, and 133,650 B for JPEG (computed from the formulas above). The factors are invented teaching constants; real sizes depend on image content, encoder, and browser support.
Use Cases
- Explaining to a team why
sizesmatters and what100vwas a fallback means. - Showing how raising quality changes the heuristic cost, and discussing when a higher value is worth it.
- Comparing the relative savings of modern formats (AVIF, WebP) over JPEG as a conversation starter.
- Prototyping the props for a hero image before applying them in your own layout.
Limitations
Security note from the registry:
- Heuristic byte estimates remain illustrative; the preview uses real
next/image.
Limits and behaviors taken from the code and configuration:
- The bundled sample is an SVG. Per the Next.js
next/imagedocs, Next.js does not optimize SVG images and treats asrcending in.svgasunoptimized. With the default/demo-hero.svgthe preview is a realnext/imagerender, but width and quality changes do not re-encode the file. To see the optimizer resize and re-encode, add a raster image (for example a JPEG or PNG) topublic/and enter its path in Sample path. - Quality allowlist.
next.config.mjsdoes not configureimages. In Next.js 16images.qualitiesdefaults to[75], and aqualityoutside the list is coerced to the closest allowed entry (development logs a warning). If you want sliders to produce distinct qualities for raster images, add aqualitieslist such as[40, 60, 75, 90, 100]to your own config. priorityis deprecated. The preview usespriority, which Next.js 16 deprecates in favor ofpreload(seepreload). The Next.js docs recommendloading="eager"orfetchPriority="high"in most cases. The snippet below usesfetchPriority="high"for an above-the-fold image.- Local paths only. The sample path must start with
/. Remote URLs are not accepted by the preview and would also requireimages.remotePatterns. - Preview width cap. The preview never renders wider than 1200 px even when the slider is higher. The slider still affects the byte table and snippet.
- Heuristic only. The byte table does not request AVIF, WebP, or JPEG variants. Actual format negotiation depends on the browser
Acceptheader and yourimages.formatsconfiguration. - Snippet differs from the preview. The copyable snippet from
nextImageExampleusessizes="(min-width: 768px) 50vw, 100vw"and your width/quality, while the preview uses the two-breakpointsizesshown above.
Use in your project
Minimal helper plus component adapted from the experiment:
// components/responsive-hero.tsx
import Image from 'next/image';
type Breakpoint = { minWidth: number; size: string };
export function buildSizesAttribute(breakpoints: Breakpoint[]): string {
const parts = [...breakpoints]
.sort((a, b) => b.minWidth - a.minWidth)
.map((b) => `(min-width: ${b.minWidth}px) ${b.size}`);
parts.push('100vw');
return parts.join(', ');
}
const sizes = buildSizesAttribute([
{ minWidth: 1024, size: '50vw' },
{ minWidth: 640, size: '75vw' },
]);
export function ResponsiveHero() {
return (
<Image
src="/hero.jpg"
alt="Hero"
width={1200}
height={675}
quality={75}
sizes={sizes}
className="h-auto w-full object-cover"
fetchPriority="high"
/>
);
}// next.config.ts - allow the quality values you actually use
import type { NextConfig } from 'next';
const config: NextConfig = {
images: {
qualities: [60, 75, 90],
},
};
export default config;Deployment
The registered Deploy button for this experiment is the platform button, which clones the repository and offers Vercel Blob, Upstash Redis, and Neon Postgres stores plus the AI_GATEWAY_API_KEY and NEXT_PUBLIC_SITE_URL environment prompts. This experiment itself uses none of those. Its only external dependency is Vercel Image Optimization, which is provided by the platform when deployed. Use the Deploy button on this experiment page.
Local Development
pnpm install
pnpm devThen open the experiment page. During next dev, images are served through the local /_next/image endpoint, so optimization behavior is similar in kind but not identical to Vercel's production pipeline. You can inspect the response of the optimizer directly for a raster image you add to public/:
curl -I "http://localhost:3000/_next/image?url=%2Fhero.jpg&w=1080&q=75"Unit tests for the helpers live in apps/experiments/image-optimization-lab/logic.test.ts:
pnpm testConfiguration
| Item | Required | Notes |
|---|---|---|
| Environment variables | No | None are read by this experiment. |
| External services | Vercel Image Optimization (when deployed) | Provided by the platform for next/image. |
images in next.config | Optional | Not set in this repo; configure qualities, formats, or remotePatterns for your own app. |
Vercel / Next.js Features Used
next/image-width,height,quality,sizes, andpriority.- Image optimization guide - responsive images and formats in the App Router.
- Vercel Image Optimization - the production pipeline behind
next/imageon Vercel. public/static assets - the sampledemo-hero.svgis served from here.