Image Optimization for the Web: The Definitive Guide

16 min read Image Transformation

Images Are the Heaviest Part of the Web

The median web page transfers over 2 MB of images. For media-heavy sites — e-commerce, real estate, travel, news — that number climbs to 5-10 MB. On a 4G mobile connection, that’s 3-8 seconds of loading just for images.

Google measures this. Largest Contentful Paint (LCP) — the time until the largest visible element renders — is a Core Web Vital that directly affects search ranking. For most pages, the largest element is an image. Slow images mean lower rankings, higher bounce rates, and fewer conversions.

Image optimization isn’t a nice-to-have. It’s the single highest-impact performance improvement for most websites. This guide covers the complete workflow: choosing formats, compressing effectively, sizing for different screens, and building automated pipelines.

Image Formats: Choosing the Right One

Four formats dominate the web. Each has a specific use case.

JPEG

The workhorse of web images since 1992. Lossy compression with a quality parameter (1-100) that controls the size-quality tradeoff. Universal browser support.

Use for: photographs, complex images with many colors, images where a small quality loss is acceptable.

Quality guidelines: 80-90 for hero images, 70-80 for thumbnails and cards, 60-70 for background images.

Limitations: no transparency, no lossless mode, compression artifacts visible at low quality settings (blocky patterns around edges).

PNG

Lossless compression. Supports transparency (alpha channel). Files are significantly larger than JPEG for photographs.

Use for: screenshots, graphics with sharp edges and text, images requiring transparency, icons and logos.

Limitations: large file sizes for photos (3-5x larger than equivalent JPEG), no lossy mode for size reduction.

WebP

Developed by Google, now supported by 97%+ of browsers. Offers both lossy and lossless modes, plus transparency. 25-34% smaller than JPEG at equivalent quality.

Use for: everything on the web where browser support isn’t a concern. WebP should be your default format.

Quality guidelines: 80-85 for general web images, 75 for thumbnails, 85-90 for hero images.

Limitations: slightly less saturated colors than JPEG at very low quality settings, some older tools don’t support it.

AVIF

The newest contender. Based on the AV1 video codec. 50% smaller than JPEG in many cases. Supports transparency, HDR, and wide color gamut. 92%+ browser support.

Use for: hero images and product photos where maximum compression matters. Best results on photographic content.

Quality guidelines: 65-75 for general use (AVIF maintains quality at lower numbers than JPEG), 75-80 for hero images.

Limitations: slower to encode than WebP, slightly less browser support, some edge cases with very detailed textures.

The Decision Framework

For most websites, the hierarchy is:

  1. Serve AVIF when the browser supports it (hero images, product photos)
  2. Fall back to WebP for broad compatibility
  3. Fall back to JPEG for legacy contexts (email, old systems)
  4. Use PNG only when you need lossless quality or transparency

Implement this with the <picture> element:

<picture>
  <source srcset="/images/hero.avif" type="image/avif">
  <source srcset="/images/hero.webp" type="image/webp">
  <img src="/images/hero.jpg" alt="Hero image" width="1200" height="800">
</picture>

Compression: Finding the Quality Sweet Spot

Compression quality has diminishing returns. The difference between quality 100 and quality 90 is invisible to most viewers. The file size difference is 40-60%.

The Quality Curve

At quality 100, you’re storing nearly every pixel perfectly. At quality 90, you’re losing imperceptible detail. At quality 80, most viewers can’t tell the difference from the original. Below quality 60, artifacts become visible — blocky patterns in JPEG, color banding in WebP.

The practical sweet spots by format:

Format High quality Standard web Thumbnails Background
JPEG 90 80-85 70-75 60-70
WebP 90 80-85 75 65-70
AVIF 80 65-75 60-65 50-60

AVIF maintains visual quality at lower numbers than JPEG and WebP — a quality 70 AVIF often looks as good as a quality 85 JPEG.

Compress to a Target File Size

Sometimes the requirement isn’t “quality 85” — it’s “under 500KB.” Email attachment limits, mobile app asset budgets, CMS upload restrictions.

The Image Transformation API has a compress_to_size operation that handles this:

import { IterationLayer } from "iterationlayer";
const client = new IterationLayer({ apiKey: "YOUR_API_KEY" });

const result = await client.transform({
  file: { type: "url", name: "photo.jpg", url: sourceUrl },
  operations: [
    { type: "compress_to_size", max_file_size_in_bytes: 500_000 },
  ],
});

The operation uses a quality-first strategy: it reduces JPEG quality from 85 down to 50. If the image is still over the target, it reduces dimensions. Quality-first preserves visual quality better than dimension-first because quality reduction removes imperceptible detail, while dimension reduction removes visible content.

Resizing: Never Serve Larger Than Display Size

A 4000x3000 photo displayed at 800x600 wastes 96% of its pixels. The browser downloads 5 MB to display 300 KB worth of content. This is the most common image performance mistake.

Resize to Display Size

If your hero image displays at 1200px wide on desktop, serve a 1200px wide image. Not the 4000px original from the photographer.

const operations = [
  { type: "resize", width_in_px: 1200, height_in_px: 800, fit: "cover" },
  { type: "convert", format: "webp", quality: 85 },
];

Fit Strategies

The fit parameter controls how the image fills the target dimensions:

  • cover — scales to fill the target, crops overflow. Result is exactly the target dimensions. Best for thumbnails, cards, hero images — anything needing exact dimensions.
  • contain — scales to fit within the target, adds letterboxing. Result is at most the target dimensions. Best when the full image must be visible.
  • inside — scales down to fit within the target, preserves aspect ratio, never upscales. Best for constraining maximum dimensions.
  • fill — stretches to exactly the target dimensions. Distorts the image. Rarely what you want.
  • outside — scales to cover the target, preserves aspect ratio, never downscales. Best for guaranteeing minimum dimensions.

For most web use cases, cover (exact dimensions, cropped) and inside (constrained, not cropped) handle everything.

Responsive Images: Different Sizes for Different Screens

A 4K monitor needs a larger image than a phone screen. Serving the desktop image to mobile wastes bandwidth. Serving the mobile image to desktop looks blurry.

srcset and sizes

HTML’s responsive image attributes let the browser choose the right image size:

<img
  srcset="/images/hero-320.webp 320w,
          /images/hero-640.webp 640w,
          /images/hero-960.webp 960w,
          /images/hero-1280.webp 1280w,
          /images/hero-1920.webp 1920w"
  sizes="100vw"
  src="/images/hero-960.webp"
  alt="Hero image"
  width="1920"
  height="1080"
>

The srcset tells the browser which sizes are available. The sizes tells the browser how wide the image will be at the current viewport. The browser picks the smallest image that covers the display area.

Generating Responsive Variants

Generate all variants from a single source image:

import { IterationLayer } from "iterationlayer";
const client = new IterationLayer({ apiKey: "YOUR_API_KEY" });

const widths = [320, 640, 960, 1280, 1920];

const variants = await Promise.all(
  widths.map(async (width) => {
    const result = await client.transform({
      file: { type: "url", name: "hero.jpg", url: sourceUrl },
      operations: [
        { type: "resize", width_in_px: width, height_in_px: Math.round(width * 0.5625), fit: "cover" },
        { type: "sharpen", sigma: 0.5 },
        { type: "convert", format: "webp", quality: 85 },
      ],
    });

    const { data: { buffer } } = result;

    return { width, buffer: Buffer.from(buffer, "base64") };
  })
);

Five sizes, WebP format, optimized quality, sharpened after downscaling. Each variant is a single API call.

Art Direction with picture

When different aspect ratios are needed for different screen sizes — landscape on desktop, square on mobile — use the <picture> element:

<picture>
  <source media="(max-width: 768px)" srcset="/images/hero-mobile.webp" type="image/webp">
  <source media="(min-width: 769px)" srcset="/images/hero-desktop.webp" type="image/webp">
  <img src="/images/hero-desktop.jpg" alt="Hero image">
</picture>

Generate the mobile variant with different dimensions or a different crop. The smart_crop operation is useful here — it detects the subject and crops around it, so the focal point is preserved across aspect ratios.

// Mobile: square crop, subject-centered
const mobileOps = [
  { type: "smart_crop", width_in_px: 640, height_in_px: 640 },
  { type: "convert", format: "webp", quality: 85 },
];

// Desktop: landscape, standard resize
const desktopOps = [
  { type: "resize", width_in_px: 1920, height_in_px: 1080, fit: "cover" },
  { type: "convert", format: "webp", quality: 85 },
];

Sharpening After Downscaling

Downscaling images softens them slightly — a 4000px image resized to 800px loses some perceived sharpness even though the content is identical. A light sharpen pass after resizing restores crispness without introducing artifacts.

const operations = [
  { type: "resize", width_in_px: 800, height_in_px: 600, fit: "cover" },
  { type: "sharpen", sigma: 0.5 },
  { type: "convert", format: "webp", quality: 85 },
];

Keep the sigma low — 0.3 to 0.7. Higher values create visible halos around edges that look worse than the softness you’re trying to fix.

Order matters: sharpen after resize, convert last. Sharpening before resize wastes effort (the resize will soften it again). Converting before the final step may introduce artifacts that get amplified by later operations.

Smart Cropping for Responsive Thumbnails

Traditional center-cropping works when the subject is centered. It fails when the subject is off to one side — a portrait where the person stands left of frame, a product photo with the item in the lower-right corner.

The smart_crop operation uses AI object detection to find the main subject before cropping:

const operations = [
  { type: "smart_crop", width_in_px: 400, height_in_px: 400 },
  { type: "convert", format: "webp", quality: 80 },
];

The result is a 400x400 image with the subject centered, regardless of where it was in the original. No coordinate math, no face detection library, no per-image manual adjustment.

Automated Pipelines

The Basic Pipeline

A production image optimization pipeline combines resizing, sharpening, and conversion:

import { IterationLayer } from "iterationlayer";
const client = new IterationLayer({ apiKey: "YOUR_API_KEY" });

const optimizeImage = async (sourceUrl: string, targetWidth: number) => {
  const result = await client.transform({
    file: { type: "url", name: "image.jpg", url: sourceUrl },
    operations: [
      { type: "resize", width_in_px: targetWidth, height_in_px: Math.round(targetWidth * 0.667), fit: "cover" },
      { type: "auto_contrast" },
      { type: "sharpen", sigma: 0.5 },
      { type: "convert", format: "webp", quality: 85 },
    ],
  });

  const { data: { buffer } } = result;

  return Buffer.from(buffer, "base64");
};

Four operations, one API call. Resize, auto-contrast, sharpen, convert. No server to maintain.

The Complete Pipeline with Variants

For a production content pipeline, generate multiple formats and sizes from each source image:

import { IterationLayer } from "iterationlayer";
const client = new IterationLayer({ apiKey: "YOUR_API_KEY" });

const generateImageSet = async (sourceUrl: string) => {
  const widths = [320, 640, 960, 1280, 1920];
  const formats = ["webp", "avif"] as const;

  const variants = await Promise.all(
    formats.flatMap((format) =>
      widths.map(async (width) => {
        const quality = format === "avif" ? 70 : 85;
        const result = await client.transform({
          file: { type: "url", name: "image.jpg", url: sourceUrl },
          operations: [
            { type: "resize", width_in_px: width, height_in_px: Math.round(width * 0.667), fit: "cover" },
            { type: "sharpen", sigma: 0.5 },
            { type: "convert", format, quality },
          ],
        });

        const { data: { buffer } } = result;

        return { width, format, buffer: Buffer.from(buffer, "base64") };
      })
    )
  );

  return variants;
};

This generates 10 variants (5 widths x 2 formats) from a single source image. Upload each variant to your CDN and reference them in srcset and <source> elements.

Measuring Performance

Core Web Vitals

Three metrics that matter for image optimization:

Largest Contentful Paint (LCP) — time until the largest element renders. Smaller images load faster. Target: under 2.5 seconds.

Cumulative Layout Shift (CLS) — layout instability caused by elements loading and shifting content. Set width and height on <img> tags so the browser reserves space before the image loads. Target: under 0.1.

First Contentful Paint (FCP) — time until any content renders. Large images on the critical rendering path delay FCP. Lazy-load below-the-fold images.

Lighthouse

Run Lighthouse audits regularly. The “Opportunities” section flags specific images that could be resized, compressed, or converted to modern formats. It estimates the byte savings for each suggestion.

Real User Monitoring

Lab tools like Lighthouse test under controlled conditions. Real user monitoring (Vercel Analytics, web-vitals library, Google CrUX) shows actual performance across real devices and connections. An image that loads fine on fiber may be the bottleneck on 3G.

Lazy Loading

Images below the fold — not visible when the page first loads — should lazy-load. The browser fetches them only when they scroll into view.

<img src="/images/product.webp" alt="Product" loading="lazy" width="400" height="400">

The loading="lazy" attribute is supported by all modern browsers. No JavaScript needed.

Do NOT lazy-load the hero image or any above-the-fold content. Those images are on the critical rendering path — they need to load immediately.

CDN Delivery

Serve optimized images from a CDN (Content Delivery Network). The CDN caches images at edge locations close to the user, reducing latency.

Most CDNs support cache headers. Set a long Cache-Control max-age on image URLs that include a content hash — the URL changes when the image changes, so stale cache isn’t a concern:

Cache-Control: public, max-age=31536000, immutable

This tells the browser and CDN to cache the image for one year without revalidation.

Putting It All Together

A complete image optimization workflow:

  1. Upload — accept the original image (any format, any size)
  2. Process — resize to target dimensions, sharpen, convert to WebP and AVIF
  3. Generate variants — multiple sizes for responsive delivery
  4. Store — upload processed variants to object storage (S3, R2, GCS)
  5. Serve — deliver through a CDN with long cache headers
  6. Reference — use srcset, sizes, and <picture> in your HTML

Steps 2-3 are the API calls. Steps 4-6 are your infrastructure. The processing itself is one API call per variant — no servers to scale, no libraries to patch, no image processing infrastructure to maintain.

Get Started

Check the docs for the full operation reference and format-specific quality guidelines. The TypeScript and Python SDKs handle file input and response parsing.

Sign up for a free account — no credit card required. Take your heaviest page image, run it through the pipeline, and measure the before-and-after file size.

Start building in minutes

Free trial included. No credit card required.