Performance 22 min read

Image Resizing for the Web: The Complete Guide

Master image resizing for web performance. Learn optimal dimensions, aspect ratios, responsive sizing strategies, and automated resizing with CDNs and build tools.

By ImageGuide Team · Published February 15, 2026
image resizingresponsive imagesperformancedimensionsaspect ratios

Oversized images are the single biggest source of wasted bandwidth on the web. A 4000x3000 photo served to a mobile device that displays it at 400px wide wastes over 95% of the downloaded pixels. This guide covers everything about resizing images correctly, from choosing the right dimensions to automating the process at scale.

Why Image Resizing Matters

Images account for roughly 50-70% of total page weight on most websites. Serving images larger than necessary directly harms performance, user experience, and business metrics.

The Real Cost of Oversized Images

ScenarioImage ServedImage NeededWasted DataLoad Impact
4K photo on mobile (375px)4000x3000 (2.8 MB)750x563 (85 KB)97% wasted+3-5s on 4G
DSLR photo as blog thumbnail6000x4000 (5.2 MB)600x400 (40 KB)99% wasted+8-12s on 4G
Unresized hero on tablet3840x2160 (1.5 MB)1536x864 (180 KB)88% wasted+2-3s on 4G
Product photo in cart widget2400x2400 (900 KB)150x150 (8 KB)99% wasted+1-2s on 4G

Core Web Vitals Impact

Google’s Core Web Vitals measure real user experience, and images are the primary factor in two of the three metrics:

LCP (Largest Contentful Paint):

  • The LCP element is an image on ~70% of web pages
  • Oversized images take longer to download, directly increasing LCP
  • Target: under 2.5 seconds
  • A 1 MB image takes approximately 2.5 seconds on a typical 4G connection

CLS (Cumulative Layout Shift):

  • Images without explicit width/height cause layout shifts when they load
  • Resized images with proper dimensions prevent CLS entirely
  • Target: under 0.1

Real-world savings:

Before optimization:
  Hero image: 3840x2160 JPEG = 1.4 MB
  LCP on 4G: ~4.2 seconds (failing)

After proper resizing:
  Hero image: 1200x675 WebP = 85 KB
  + srcset for other sizes
  LCP on 4G: ~1.1 seconds (passing)

Savings: 94% bandwidth reduction

Business Impact

MetricPer 1s Load Improvement
Bounce rate-7% reduction
Conversion rate+2% increase
Page views per session+11% increase
Mobile revenue+8.4% (Walmart study)
SEO rankingDirect Core Web Vitals factor

Common Web Image Dimensions

Different contexts require different image sizes. Here is a comprehensive reference for the most common web image dimensions.

Standard Web Image Sizes

Use CaseRecommended Size (px)Aspect RatioNotes
Hero banner (full-width)1920x108016:9Provide 1200, 1920, 2560 for responsive
Hero banner (contained)1200x6002:1Common in blog/marketing layouts
Blog featured image1200x6301.91:1Matches OG image ratio
Blog content image800x45016:9In-content width
Product image (main)800x8001:1Square for consistent grid
Product image (zoom)1600x16001:1Loaded on demand
Product thumbnail400x4001:1Category/listing pages
Cart thumbnail150x1501:1Mini cart and order summary
Avatar200x2001:1User profiles
Avatar (small)48x481:1Comments, chat
Logo250x80 (max)VariesKeep under 300px wide
Favicon32x321:1Browser tab
Apple touch icon180x1801:1iOS home screen
Icon (general)24x24 to 64x641:1UI elements

Social Media and OG Image Sizes

Platform / ContextSize (px)Aspect RatioFormat
Open Graph (og:image)1200x6301.91:1JPEG or PNG
Twitter card (large)1200x6281.91:1JPEG or PNG
Twitter card (summary)240x2401:1JPEG or PNG
Facebook shared image1200x6301.91:1JPEG or PNG
Instagram post (square)1080x10801:1JPEG
Instagram post (portrait)1080x13504:5JPEG
Instagram story1080x19209:16JPEG or PNG
LinkedIn post1200x6271.91:1JPEG or PNG
Pinterest pin1000x15002:3JPEG

Email Image Dimensions

ElementRecommended WidthNotes
Email header/banner600pxStandard email width
Full-width email image600pxMatches container
Half-width email image280-290pxTwo-column layout
Email product image200-250pxProduct grid
Email logo200px maxKeep crisp at 1x and 2x

Note: Email clients have limited support for responsive images. Design for 600px max width and provide 2x assets for retina displays.

Understanding Aspect Ratios

Aspect ratios define the proportional relationship between width and height. Using consistent ratios prevents awkward cropping and layout shifts.

Common Aspect Ratios

RatioDecimalCommon Uses
16:91.778Hero banners, video thumbnails, YouTube
4:31.333Traditional photos, presentations, iPad
1:11.000Product images, avatars, Instagram squares
3:21.500DSLR photos, standard prints, Flickr
1.91:11.910Open Graph images, Twitter cards
2:12.000Panoramic banners
4:50.800Instagram portrait, product lifestyle
2:30.667Pinterest pins, portrait photos
9:160.563Stories, vertical video, mobile full-screen
21:92.333Ultra-wide banners, cinema

CSS aspect-ratio Property

The CSS aspect-ratio property prevents layout shift by reserving space before the image loads:

/* Fixed aspect ratio container */
.hero-image {
  aspect-ratio: 16 / 9;
  width: 100%;
  object-fit: cover;
}

/* Product image - always square */
.product-image {
  aspect-ratio: 1 / 1;
  width: 100%;
  object-fit: contain;
  background: #f5f5f5;
}

/* OG-ratio banner */
.og-banner {
  aspect-ratio: 1.91 / 1;
  width: 100%;
  object-fit: cover;
}

/* Responsive with max dimensions */
.blog-image {
  aspect-ratio: 16 / 9;
  width: 100%;
  max-width: 800px;
  object-fit: cover;
}

object-fit Options

When the image aspect ratio does not match the container:

/* Cover - fills container, may crop edges */
.cover { object-fit: cover; }

/* Contain - fits inside container, may show background */
.contain { object-fit: contain; }

/* Fill - stretches to fill (distorts) */
.fill { object-fit: fill; }

/* Scale-down - like contain but never scales up */
.scale-down { object-fit: scale-down; }
object-fitBehaviorBest For
coverFills container, crops excessHero images, backgrounds, thumbnails
containFits inside, preserves ratioProduct images, logos
fillStretches to fill (distorts!)Almost never use this
scale-downLike contain, never enlargesUser-uploaded images
noneNo resizing at allPixel-perfect sprites

Calculating Dimensions from Ratio

function calculateDimensions(targetWidth, aspectRatio) {
  // aspectRatio as "16:9" string
  const [w, h] = aspectRatio.split(':').map(Number);
  const targetHeight = Math.round(targetWidth * (h / w));
  return { width: targetWidth, height: targetHeight };
}

// Examples
calculateDimensions(1200, '16:9');  // { width: 1200, height: 675 }
calculateDimensions(800, '1:1');    // { width: 800, height: 800 }
calculateDimensions(1200, '1.91:1'); // { width: 1200, height: 628 }
calculateDimensions(1000, '2:3');   // { width: 1000, height: 1500 }

Retina and HiDPI Considerations

Modern displays have device pixel ratios (DPR) greater than 1, meaning each CSS pixel maps to multiple physical pixels. Images must account for this to appear sharp.

Device Pixel Ratios

DPRDevicesImage Size Needed for 400px Display
1xOlder monitors, budget laptops400px
1.5xSome Windows devices600px
2xiPhones, MacBooks, most Android800px
3xiPhone Plus/Max, flagship Android1200px

Responsive Images with Density Descriptors

For fixed-size images (logos, icons, avatars):

<img
  src="avatar-96.jpg"
  srcset="
    avatar-96.jpg 1x,
    avatar-192.jpg 2x,
    avatar-288.jpg 3x
  "
  width="96"
  height="96"
  alt="User avatar"
>

Responsive Images with Width Descriptors

For fluid images that change size with the viewport:

<img
  src="hero-1200.jpg"
  srcset="
    hero-400.jpg 400w,
    hero-800.jpg 800w,
    hero-1200.jpg 1200w,
    hero-1600.jpg 1600w,
    hero-2400.jpg 2400w
  "
  sizes="
    (max-width: 600px) 100vw,
    (max-width: 1200px) 80vw,
    1200px
  "
  alt="Hero banner"
  width="1200"
  height="675"
>

How browsers select the right image:

  1. Parse sizes to determine the display width (e.g., 600px viewport = 100vw = 600px)
  2. Multiply by device pixel ratio (600px x 2 DPR = 1200px needed)
  3. Select the smallest image from srcset that is at least 1200px wide

When to Provide Each Density

Use Case1x2x3xReasoning
Hero/bannerYesYesOptionalLarge savings on 1x devices
Content imagesYesYesNo3x is rare on desktop
Product imagesYesYesYesMobile e-commerce is critical
ThumbnailsYesYesNoSmall files anyway
Icons/logosYesYesYesTiny files, sharpness matters

DPR-Aware Sizing Function

function getResponsiveSizes(baseWidth, options = {}) {
  const { maxDpr = 2, widths = null } = options;

  if (widths) return widths;

  const sizes = [baseWidth];
  if (maxDpr >= 1.5) sizes.push(Math.round(baseWidth * 1.5));
  if (maxDpr >= 2) sizes.push(baseWidth * 2);
  if (maxDpr >= 3) sizes.push(baseWidth * 3);

  return [...new Set(sizes)].sort((a, b) => a - b);
}

// Hero banner: base 1200px, up to 2x
getResponsiveSizes(1200);
// [1200, 1800, 2400]

// Thumbnail: base 200px, up to 3x
getResponsiveSizes(200, { maxDpr: 3 });
// [200, 300, 400, 600]

Resizing Techniques Compared

Not all resizing algorithms produce the same results. The choice of algorithm affects image quality, processing speed, and file size.

Algorithm Comparison

AlgorithmQuality (downscale)Quality (upscale)SpeedBest For
Nearest neighborPoor (pixelated)Poor (blocky)FastestPixel art, retro effects
BilinearGoodFairFastReal-time resizing, previews
BicubicVery goodGoodModerateGeneral-purpose resizing
LanczosExcellentGoodSlowerFinal production images
Mitchell-NetravaliExcellentVery goodSlowerPhotos, detailed images

Visual Quality Differences

Downscaling 4000px → 400px:

Nearest neighbor:  Jagged edges, moire patterns, lost detail
Bilinear:          Smooth but slightly soft, minimal artifacts
Bicubic:           Sharp with good detail, slight ringing on edges
Lanczos:           Sharpest detail preservation, best for photos
Mitchell:          Similar to Lanczos, slightly less ringing

Algorithm Selection Guide

Is this pixel art or icons?
  └─ Yes → Nearest neighbor (preserves hard edges)
  └─ No
       ├─ Real-time / client-side?
       │    └─ Bilinear (fast, acceptable quality)
       └─ Server-side / build-time?
            ├─ Photos → Lanczos (best detail)
            ├─ Graphics with text → Mitchell (less ringing)
            └─ General → Lanczos (default choice)

Algorithm in Different Tools

ToolDefault AlgorithmOthers Available
Sharp (Node.js)Lanczos3nearest, cubic, mitchell, lanczos2/3
Pillow (Python)BICUBIC (resize)NEAREST, BILINEAR, LANCZOS, BOX, HAMMING
ImageMagickMitchellover 30 filters including Lanczos, Catrom
CSS/browserVaries by browserimage-rendering: pixelated for nearest neighbor
Sirv CDNLanczosAutomatic best-quality

Server-Side Resizing

Sharp (Node.js)

Sharp is the fastest Node.js image processing library, built on libvips.

const sharp = require('sharp');

// Basic resize
await sharp('input.jpg')
  .resize(800, 600)
  .toFile('output.jpg');

// Resize to width, auto-calculate height
await sharp('input.jpg')
  .resize({ width: 800 })
  .toFile('output.jpg');

// Resize with aspect ratio preservation
await sharp('input.jpg')
  .resize(800, 600, {
    fit: 'inside',       // Fit inside 800x600 box
    withoutEnlargement: true  // Never upscale
  })
  .toFile('output.jpg');

// Cover (crop to fill dimensions)
await sharp('input.jpg')
  .resize(800, 800, {
    fit: 'cover',
    position: 'attention' // Smart crop using saliency detection
  })
  .toFile('output-square.jpg');

// Contain (fit inside, pad with background color)
await sharp('input.jpg')
  .resize(800, 800, {
    fit: 'contain',
    background: { r: 255, g: 255, b: 255 }
  })
  .toFile('output-padded.jpg');

Generate multiple sizes for responsive images:

const sharp = require('sharp');
const path = require('path');

async function generateResponsiveSizes(inputPath, outputDir, sizes) {
  const filename = path.parse(inputPath).name;

  for (const width of sizes) {
    for (const format of ['avif', 'webp', 'jpg']) {
      let pipeline = sharp(inputPath)
        .resize(width, null, {
          withoutEnlargement: true,
          kernel: sharp.kernel.lanczos3
        });

      const outputPath = `${outputDir}/${filename}-${width}.${format}`;

      if (format === 'avif') {
        await pipeline.avif({ quality: 65, effort: 4 }).toFile(outputPath);
      } else if (format === 'webp') {
        await pipeline.webp({ quality: 80 }).toFile(outputPath);
      } else {
        await pipeline.jpeg({ quality: 82, mozjpeg: true }).toFile(outputPath);
      }
    }
  }
}

// Generate hero image sizes
await generateResponsiveSizes(
  './originals/hero.jpg',
  './output',
  [400, 800, 1200, 1600, 2400]
);

// Generate product image sizes
await generateResponsiveSizes(
  './originals/product.jpg',
  './output',
  [150, 400, 800, 1600]
);

Pillow (Python)

from PIL import Image

def resize_image(input_path, output_path, target_width, target_height=None):
    """Resize image maintaining aspect ratio."""
    with Image.open(input_path) as img:
        if target_height is None:
            # Calculate height from width
            ratio = target_width / img.width
            target_height = round(img.height * ratio)

        resized = img.resize(
            (target_width, target_height),
            Image.LANCZOS  # Best quality
        )
        resized.save(output_path, quality=85, optimize=True)

# Basic resize
resize_image('photo.jpg', 'photo-800.jpg', 800)

# Generate multiple sizes
def generate_sizes(input_path, output_dir, sizes):
    """Generate multiple sizes for responsive images."""
    import os

    with Image.open(input_path) as img:
        name = os.path.splitext(os.path.basename(input_path))[0]

        for width in sizes:
            if width > img.width:
                continue  # Never upscale

            ratio = width / img.width
            height = round(img.height * ratio)

            resized = img.resize((width, height), Image.LANCZOS)

            # Save as JPEG
            jpeg_path = os.path.join(output_dir, f'{name}-{width}.jpg')
            resized.save(jpeg_path, 'JPEG', quality=85, optimize=True)

            # Save as WebP
            webp_path = os.path.join(output_dir, f'{name}-{width}.webp')
            resized.save(webp_path, 'WebP', quality=80)

generate_sizes('hero.jpg', './output', [400, 800, 1200, 1600, 2400])

Smart Cropping with Python

from PIL import Image
import numpy as np

def smart_crop(input_path, output_path, target_width, target_height):
    """Crop to target dimensions focusing on the most interesting region."""
    with Image.open(input_path) as img:
        # Calculate crop dimensions
        img_ratio = img.width / img.height
        target_ratio = target_width / target_height

        if img_ratio > target_ratio:
            # Image is wider - crop sides
            new_width = int(img.height * target_ratio)
            left = (img.width - new_width) // 2
            box = (left, 0, left + new_width, img.height)
        else:
            # Image is taller - crop top/bottom
            new_height = int(img.width / target_ratio)
            top = (img.height - new_height) // 3  # Rule of thirds: favor top
            box = (0, top, img.width, top + new_height)

        cropped = img.crop(box)
        resized = cropped.resize(
            (target_width, target_height),
            Image.LANCZOS
        )
        resized.save(output_path, quality=85)

# Crop to 16:9 hero banner
smart_crop('photo.jpg', 'hero.jpg', 1200, 675)

# Crop to square thumbnail
smart_crop('photo.jpg', 'thumb.jpg', 400, 400)

ImageMagick CLI

# Resize to width, maintain aspect ratio
convert input.jpg -resize 800x output.jpg

# Resize to exact dimensions (may distort)
convert input.jpg -resize 800x600! output.jpg

# Resize to fit inside box (no distortion)
convert input.jpg -resize 800x600 output.jpg

# Resize to fill box, then crop
convert input.jpg -resize 800x600^ -gravity center -extent 800x600 output.jpg

# Resize only if larger (never upscale)
convert input.jpg -resize '800x600>' output.jpg

# Batch resize all JPEGs to max 1200px width
mogrify -resize '1200x>' -quality 85 *.jpg

# Generate thumbnails with center crop
mogrify -resize 400x400^ -gravity center -extent 400x400 \
  -path thumbnails *.jpg

# Batch with specific Lanczos filter
convert input.jpg -filter Lanczos -resize 800x -quality 85 output.jpg

CDN-Based Resizing

CDN-based resizing transforms images on the fly via URL parameters. This is the most efficient approach for most websites because you store a single high-resolution original and generate any size on demand.

Sirv CDN

Sirv provides real-time image resizing, format conversion, and optimization through URL parameters:

<!-- Resize to 800px width -->
<img src="https://your-account.sirv.com/photos/hero.jpg?w=800">

<!-- Resize to exact dimensions -->
<img src="https://your-account.sirv.com/photos/hero.jpg?w=800&h=600">

<!-- Resize and crop to fill -->
<img src="https://your-account.sirv.com/photos/hero.jpg?w=800&h=600&scale.option=fill">

<!-- Square product thumbnail with padding -->
<img src="https://your-account.sirv.com/products/shoe.jpg?w=400&h=400&canvas.width=400&canvas.height=400&canvas.color=white&canvas.position=center">

<!-- Auto-format (serves AVIF, WebP, or JPEG based on browser) -->
<img src="https://your-account.sirv.com/photos/hero.jpg?w=800&format=optimal">

<!-- Quality control -->
<img src="https://your-account.sirv.com/photos/hero.jpg?w=800&q=80&format=optimal">

Responsive images with Sirv:

<img
  src="https://your-account.sirv.com/photos/hero.jpg?w=800&format=optimal"
  srcset="
    https://your-account.sirv.com/photos/hero.jpg?w=400&format=optimal 400w,
    https://your-account.sirv.com/photos/hero.jpg?w=800&format=optimal 800w,
    https://your-account.sirv.com/photos/hero.jpg?w=1200&format=optimal 1200w,
    https://your-account.sirv.com/photos/hero.jpg?w=1600&format=optimal 1600w,
    https://your-account.sirv.com/photos/hero.jpg?w=2400&format=optimal 2400w
  "
  sizes="(max-width: 600px) 100vw, (max-width: 1200px) 80vw, 1200px"
  alt="Hero banner"
  width="1200"
  height="675"
>

Sirv responsive helper function:

function sirvResponsive(path, options = {}) {
  const {
    baseUrl = 'https://your-account.sirv.com',
    widths = [400, 800, 1200, 1600, 2400],
    quality = 80,
    format = 'optimal',
    sizes = '100vw'
  } = options;

  const srcset = widths
    .map(w => `${baseUrl}${path}?w=${w}&q=${quality}&format=${format} ${w}w`)
    .join(',\n    ');

  const defaultWidth = widths[Math.floor(widths.length / 2)];
  const src = `${baseUrl}${path}?w=${defaultWidth}&q=${quality}&format=${format}`;

  return { src, srcset, sizes };
}

// Usage
const hero = sirvResponsive('/photos/hero.jpg', {
  widths: [400, 800, 1200, 1600, 2400],
  sizes: '(max-width: 600px) 100vw, (max-width: 1200px) 80vw, 1200px'
});

// Output ready for <img> or framework component
console.log(hero.srcset);

Sign up for Sirv →

Cloudinary

<!-- Resize to 800px width -->
<img src="https://res.cloudinary.com/demo/image/upload/w_800/sample.jpg">

<!-- Crop to fill 800x600 -->
<img src="https://res.cloudinary.com/demo/image/upload/w_800,h_600,c_fill/sample.jpg">

<!-- Auto quality and format -->
<img src="https://res.cloudinary.com/demo/image/upload/w_800,q_auto,f_auto/sample.jpg">

<!-- Smart crop with AI focus -->
<img src="https://res.cloudinary.com/demo/image/upload/w_400,h_400,c_fill,g_auto/sample.jpg">

Imgix

<!-- Resize to width -->
<img src="https://your-source.imgix.net/photo.jpg?w=800">

<!-- Crop to fill -->
<img src="https://your-source.imgix.net/photo.jpg?w=800&h=600&fit=crop">

<!-- Auto format and quality -->
<img src="https://your-source.imgix.net/photo.jpg?w=800&auto=format,compress">

<!-- Face-aware crop for avatars -->
<img src="https://your-source.imgix.net/portrait.jpg?w=200&h=200&fit=facearea&facepad=2">

CDN Comparison

FeatureSirvCloudinaryImgix
URL-based resizingYesYesYes
Auto format (AVIF/WebP)Yes (format=optimal)Yes (f_auto)Yes (auto=format)
Smart cropYesYes (AI)Yes (face detection)
Free tier500 MB storage, 2 GB transfer25 credits/moNone (trial only)
Background removalVia AI StudioYes (add-on)No
360 spin / zoom viewerYes (Media Viewer)NoNo
Edge cachingGlobal CDNGlobal CDNGlobal CDN

Build-Time Resizing

For static sites and JAMstack applications, resizing images at build time ensures zero runtime processing cost.

Astro Image Optimization

Astro has built-in image optimization:

---
import { Image } from 'astro:assets';
import heroImage from '../assets/hero.jpg';
---

<!-- Astro automatically resizes and optimizes -->
<Image
  src={heroImage}
  widths={[400, 800, 1200]}
  sizes="(max-width: 600px) 100vw, (max-width: 1200px) 80vw, 1200px"
  alt="Hero banner"
/>

Astro generates multiple sizes and formats at build time, producing HTML with proper srcset attributes.

Vite with vite-imagetools

// vite.config.js
import { defineConfig } from 'vite';
import { imagetools } from 'vite-imagetools';

export default defineConfig({
  plugins: [
    imagetools({
      defaultDirectives: (url) => {
        if (url.searchParams.has('hero')) {
          return new URLSearchParams({
            w: '400;800;1200;1600',
            format: 'avif;webp;jpg',
            quality: '80',
            as: 'srcset'
          });
        }
        return new URLSearchParams();
      }
    })
  ]
});

Usage in code:

// Import with query parameters
import heroSrcset from './hero.jpg?w=400;800;1200;1600&format=avif;webp;jpg&as=srcset';
import thumbUrl from './thumb.jpg?w=200&format=webp';

Next.js Image Component

import Image from 'next/image';

// Automatic resizing and optimization
export default function HeroSection() {
  return (
    <Image
      src="/photos/hero.jpg"
      alt="Hero banner"
      width={1200}
      height={675}
      sizes="(max-width: 768px) 100vw, (max-width: 1200px) 80vw, 1200px"
      priority  // Preload for LCP
    />
  );
}

// With external image CDN (like Sirv)
// next.config.js
module.exports = {
  images: {
    remotePatterns: [
      {
        protocol: 'https',
        hostname: '*.sirv.com'
      }
    ],
    // Or use Sirv as the image loader
    loader: 'custom',
    loaderFile: './sirv-loader.js'
  }
};

Custom Sirv loader for Next.js:

// sirv-loader.js
export default function sirvLoader({ src, width, quality }) {
  const params = new URLSearchParams({
    w: width,
    q: quality || 80,
    format: 'optimal'
  });
  return `https://your-account.sirv.com${src}?${params}`;
}

Webpack with responsive-loader

// webpack.config.js
module.exports = {
  module: {
    rules: [
      {
        test: /\.(jpg|png)$/,
        use: {
          loader: 'responsive-loader',
          options: {
            adapter: require('responsive-loader/sharp'),
            sizes: [400, 800, 1200, 1600],
            format: 'webp',
            quality: 80,
            placeholder: true,
            placeholderSize: 20
          }
        }
      }
    ]
  }
};

Client-Side Resizing

Client-side resizing is useful for image uploads, previews, and interactive tools. Avoid it for serving content images since it requires downloading the full-size original first.

Canvas API

function resizeImage(file, maxWidth, maxHeight, quality = 0.85) {
  return new Promise((resolve) => {
    const img = new Image();
    const canvas = document.createElement('canvas');
    const ctx = canvas.getContext('2d');

    img.onload = () => {
      // Calculate new dimensions
      let { width, height } = img;
      if (width > maxWidth) {
        height = Math.round(height * (maxWidth / width));
        width = maxWidth;
      }
      if (height > maxHeight) {
        width = Math.round(width * (maxHeight / height));
        height = maxHeight;
      }

      canvas.width = width;
      canvas.height = height;

      // Use high-quality smoothing
      ctx.imageSmoothingEnabled = true;
      ctx.imageSmoothingQuality = 'high';

      ctx.drawImage(img, 0, 0, width, height);

      canvas.toBlob(
        (blob) => resolve(blob),
        'image/jpeg',
        quality
      );
    };

    img.src = URL.createObjectURL(file);
  });
}

// Usage: Resize avatar upload before sending to server
const fileInput = document.querySelector('#avatar-upload');
fileInput.addEventListener('change', async (e) => {
  const file = e.target.files[0];
  const resized = await resizeImage(file, 400, 400);

  // Upload the resized blob
  const formData = new FormData();
  formData.append('avatar', resized, 'avatar.jpg');
  await fetch('/api/upload', { method: 'POST', body: formData });
});

OffscreenCanvas for Background Processing

// Worker thread (resize-worker.js)
self.onmessage = async (e) => {
  const { imageBitmap, width, height, quality } = e.data;

  const canvas = new OffscreenCanvas(width, height);
  const ctx = canvas.getContext('2d');

  ctx.imageSmoothingEnabled = true;
  ctx.imageSmoothingQuality = 'high';
  ctx.drawImage(imageBitmap, 0, 0, width, height);

  const blob = await canvas.convertToBlob({
    type: 'image/webp',
    quality: quality || 0.85
  });

  self.postMessage({ blob });
};

// Main thread
async function resizeInWorker(file, width, height) {
  const bitmap = await createImageBitmap(file);
  const worker = new Worker('resize-worker.js');

  return new Promise((resolve) => {
    worker.onmessage = (e) => {
      resolve(e.data.blob);
      worker.terminate();
    };
    worker.postMessage(
      { imageBitmap: bitmap, width, height },
      [bitmap]  // Transfer ownership for performance
    );
  });
}

When Client-Side Resizing Makes Sense

Use CaseClient-SideServer/CDN
Avatar upload previewYesFinal processing on server
Image crop/edit toolYesSave result to server
Thumbnail preview before uploadYesNot needed
Content images for displayNoYes (always)
Responsive image deliveryNoYes (CDN)
Batch gallery uploadYes (reduce upload size)Final processing on server

Responsive Sizing Strategy

A complete responsive image strategy combines proper sizing, format selection, and delivery optimization.

Breakpoint-Based Sizes

Define image widths at common breakpoints:

<img
  src="article-800.jpg"
  srcset="
    article-400.jpg 400w,
    article-600.jpg 600w,
    article-800.jpg 800w,
    article-1000.jpg 1000w,
    article-1200.jpg 1200w,
    article-1600.jpg 1600w
  "
  sizes="
    (max-width: 640px) 100vw,
    (max-width: 1024px) calc(100vw - 4rem),
    (max-width: 1280px) calc(100vw - 20rem),
    800px
  "
  alt="Article image"
  width="800"
  height="450"
  loading="lazy"
>

Art Direction with picture Element

When different crops or compositions are needed at different breakpoints:

<picture>
  <!-- Mobile: square crop, product focus -->
  <source
    media="(max-width: 600px)"
    srcset="product-mobile-400.avif 400w, product-mobile-800.avif 800w"
    sizes="100vw"
    type="image/avif"
  >
  <source
    media="(max-width: 600px)"
    srcset="product-mobile-400.webp 400w, product-mobile-800.webp 800w"
    sizes="100vw"
    type="image/webp"
  >

  <!-- Desktop: wide banner with context -->
  <source
    srcset="product-wide-800.avif 800w, product-wide-1200.avif 1200w, product-wide-1600.avif 1600w"
    sizes="(max-width: 1200px) 100vw, 1200px"
    type="image/avif"
  >
  <source
    srcset="product-wide-800.webp 800w, product-wide-1200.webp 1200w, product-wide-1600.webp 1600w"
    sizes="(max-width: 1200px) 100vw, 1200px"
    type="image/webp"
  >

  <img
    src="product-wide-1200.jpg"
    alt="Product lifestyle shot"
    width="1200"
    height="675"
  >
</picture>

Container Queries for Image Sizing

Modern CSS container queries let images adapt to their container rather than the viewport:

.card {
  container-type: inline-size;
}

.card img {
  width: 100%;
  aspect-ratio: 16 / 9;
  object-fit: cover;
}

/* Small card: lower quality is fine */
@container (max-width: 300px) {
  .card img {
    aspect-ratio: 1 / 1;
  }
}

/* Large card: show full banner ratio */
@container (min-width: 600px) {
  .card img {
    aspect-ratio: 21 / 9;
  }
}

Performance Budgets

Set image size budgets based on connection speed targets:

ConnectionBudget per ImageTotal Page Images
Slow 3G (400 kbps)< 50 KB< 200 KB total
Fast 3G (1.6 Mbps)< 100 KB< 500 KB total
4G (10 Mbps)< 200 KB< 1 MB total
WiFi/5G (50+ Mbps)< 500 KB< 2 MB total
// Check image budget compliance
function checkImageBudget(images, budgetKB = 200) {
  const results = images.map(img => ({
    src: img.src,
    sizeKB: Math.round(img.size / 1024),
    overBudget: img.size / 1024 > budgetKB
  }));

  const totalKB = results.reduce((sum, r) => sum + r.sizeKB, 0);

  return {
    images: results,
    totalKB,
    overBudget: results.filter(r => r.overBudget),
    recommendation: totalKB > 1000
      ? 'Consider lazy loading below-fold images'
      : 'Within budget'
  };
}

Automated Resizing Workflows

On-Upload Resize Pipeline

Process images the moment they are uploaded to generate all required sizes:

const sharp = require('sharp');
const path = require('path');

const IMAGE_PRESETS = {
  hero: {
    widths: [400, 800, 1200, 1600, 2400],
    formats: ['avif', 'webp', 'jpg'],
    quality: { avif: 60, webp: 80, jpg: 82 },
    maxHeight: null
  },
  product: {
    widths: [150, 400, 800, 1600],
    formats: ['avif', 'webp', 'png'],
    quality: { avif: 65, webp: 82, png: null },
    aspectRatio: '1:1',
    fit: 'contain',
    background: { r: 255, g: 255, b: 255 }
  },
  thumbnail: {
    widths: [200, 400],
    formats: ['avif', 'webp', 'jpg'],
    quality: { avif: 55, webp: 75, jpg: 78 },
    aspectRatio: '16:9',
    fit: 'cover'
  },
  avatar: {
    widths: [48, 96, 192],
    formats: ['avif', 'webp', 'jpg'],
    quality: { avif: 60, webp: 80, jpg: 80 },
    aspectRatio: '1:1',
    fit: 'cover'
  }
};

async function processUpload(inputBuffer, preset, outputDir, name) {
  const config = IMAGE_PRESETS[preset];
  if (!config) throw new Error(`Unknown preset: ${preset}`);

  const results = [];

  for (const width of config.widths) {
    let height = null;
    if (config.aspectRatio) {
      const [rw, rh] = config.aspectRatio.split(':').map(Number);
      height = Math.round(width * (rh / rw));
    }

    for (const format of config.formats) {
      let pipeline = sharp(inputBuffer)
        .resize(width, height, {
          fit: config.fit || 'inside',
          withoutEnlargement: true,
          background: config.background || { r: 0, g: 0, b: 0, alpha: 0 }
        });

      const quality = config.quality[format];
      const outputPath = path.join(outputDir, `${name}-${width}.${format}`);

      if (format === 'avif') {
        pipeline = pipeline.avif({ quality, effort: 4 });
      } else if (format === 'webp') {
        pipeline = pipeline.webp({ quality });
      } else if (format === 'jpg') {
        pipeline = pipeline.jpeg({ quality, mozjpeg: true });
      } else if (format === 'png') {
        pipeline = pipeline.png({ compressionLevel: 9 });
      }

      const output = await pipeline.toFile(outputPath);
      results.push({
        path: outputPath,
        width: output.width,
        height: output.height,
        size: output.size,
        format
      });
    }
  }

  return results;
}

CI/CD Integration

Resize images as part of your build pipeline:

# .github/workflows/optimize-images.yml
name: Optimize Images

on:
  push:
    paths:
      - 'content/images/**'

jobs:
  optimize:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - name: Setup Node.js
        uses: actions/setup-node@v4
        with:
          node-version: '20'

      - name: Install dependencies
        run: npm install sharp glob

      - name: Resize and optimize images
        run: node scripts/resize-images.js

      - name: Commit optimized images
        run: |
          git config user.name "Image Bot"
          git config user.email "bot@example.com"
          git add content/images/optimized/
          git diff --staged --quiet || git commit -m "Optimize images"
          git push

The resize script:

// scripts/resize-images.js
const sharp = require('sharp');
const { globSync } = require('glob');
const path = require('path');
const fs = require('fs');

const INPUT_DIR = 'content/images/originals';
const OUTPUT_DIR = 'content/images/optimized';

const SIZES = [400, 800, 1200, 1600];
const FORMATS = ['avif', 'webp', 'jpg'];

async function main() {
  const images = globSync(`${INPUT_DIR}/**/*.{jpg,jpeg,png}`);
  console.log(`Found ${images.length} images to process`);

  for (const imagePath of images) {
    const name = path.parse(imagePath).name;
    const relDir = path.relative(INPUT_DIR, path.dirname(imagePath));
    const outDir = path.join(OUTPUT_DIR, relDir);
    fs.mkdirSync(outDir, { recursive: true });

    for (const width of SIZES) {
      for (const format of FORMATS) {
        const outPath = path.join(outDir, `${name}-${width}.${format}`);

        // Skip if already exists and source is older
        if (fs.existsSync(outPath)) {
          const srcStat = fs.statSync(imagePath);
          const outStat = fs.statSync(outPath);
          if (srcStat.mtimeMs <= outStat.mtimeMs) continue;
        }

        let pipeline = sharp(imagePath)
          .resize(width, null, { withoutEnlargement: true });

        if (format === 'avif') pipeline = pipeline.avif({ quality: 65 });
        else if (format === 'webp') pipeline = pipeline.webp({ quality: 80 });
        else pipeline = pipeline.jpeg({ quality: 82, mozjpeg: true });

        await pipeline.toFile(outPath);
      }
    }

    console.log(`Processed: ${name}`);
  }

  console.log('Done');
}

main().catch(console.error);

CDN Auto-Resize

Instead of generating files yourself, let the CDN handle resizing on the fly. Upload one high-resolution original, and serve any size via URL parameters.

// Upload original to Sirv, serve any size
function getImageUrl(imagePath, width, options = {}) {
  const { quality = 80, format = 'optimal', height } = options;

  const params = new URLSearchParams({ w: width, q: quality, format });
  if (height) params.set('h', height);

  return `https://your-account.sirv.com${imagePath}?${params}`;
}

// Single source, infinite sizes
const hero1200 = getImageUrl('/photos/hero.jpg', 1200);
const hero400 = getImageUrl('/photos/hero.jpg', 400);
const thumb = getImageUrl('/photos/hero.jpg', 200, { height: 200 });

This eliminates the need for build-time or upload-time processing entirely. The CDN generates the requested size on the first request and caches it globally.

Get started with Sirv CDN →

Common Resizing Mistakes

1. Upscaling Small Images

Enlarging a small image creates blurry, pixelated results. No algorithm can add detail that does not exist.

// WRONG: Upscaling 400px image to 1600px
await sharp('small-400.jpg')
  .resize(1600)  // This will look terrible
  .toFile('blurry-upscale.jpg');

// RIGHT: Prevent upscaling
await sharp('small-400.jpg')
  .resize(1600, null, { withoutEnlargement: true })
  .toFile('correct.jpg');
// Output: stays at 400px width

2. Ignoring Aspect Ratio

Forcing dimensions without maintaining the aspect ratio distorts the image.

# WRONG: Force exact dimensions (distorts)
convert photo.jpg -resize 800x600! distorted.jpg

# RIGHT: Fit inside box (preserves ratio)
convert photo.jpg -resize 800x600 correct.jpg

# RIGHT: Fill box and crop (preserves ratio)
convert photo.jpg -resize 800x600^ -gravity center -extent 800x600 cropped.jpg

3. Not Providing width and height Attributes

Missing dimensions cause layout shifts (CLS):

<!-- WRONG: No dimensions - causes layout shift -->
<img src="photo.jpg" alt="Photo">

<!-- RIGHT: Explicit dimensions for aspect ratio calculation -->
<img src="photo.jpg" alt="Photo" width="800" height="600">

<!-- RIGHT: With CSS sizing -->
<img
  src="photo.jpg"
  alt="Photo"
  width="800"
  height="600"
  style="width: 100%; height: auto;"
>

4. Ignoring Device Pixel Ratio

Serving only 1x images looks blurry on retina displays:

<!-- WRONG: Single size for all devices -->
<img src="photo-800.jpg" width="800" alt="Photo">

<!-- RIGHT: Multiple densities -->
<img
  src="photo-800.jpg"
  srcset="
    photo-400.jpg 400w,
    photo-800.jpg 800w,
    photo-1200.jpg 1200w,
    photo-1600.jpg 1600w
  "
  sizes="(max-width: 800px) 100vw, 800px"
  width="800"
  height="600"
  alt="Photo"
>

5. Not Using Responsive Images

Serving one large image to all devices wastes bandwidth on mobile:

<!-- WRONG: Same 2400px image for all devices -->
<img src="hero-2400.jpg" alt="Hero">

<!-- RIGHT: Responsive with format fallback -->
<picture>
  <source
    srcset="hero-400.avif 400w, hero-800.avif 800w, hero-1200.avif 1200w, hero-1600.avif 1600w"
    sizes="100vw"
    type="image/avif"
  >
  <source
    srcset="hero-400.webp 400w, hero-800.webp 800w, hero-1200.webp 1200w, hero-1600.webp 1600w"
    sizes="100vw"
    type="image/webp"
  >
  <img
    src="hero-1200.jpg"
    srcset="hero-400.jpg 400w, hero-800.jpg 800w, hero-1200.jpg 1200w, hero-1600.jpg 1600w"
    sizes="100vw"
    alt="Hero banner"
    width="1600"
    height="900"
  >
</picture>

6. Resizing in CSS Instead of at the Source

/* WRONG: Browser downloads 4000px and CSS scales it down */
.hero {
  width: 100%;
  max-width: 1200px;
}
/* The full 4000px image is still downloaded */

/* This CSS is fine, but the src must be properly sized */
.hero {
  width: 100%;
  max-width: 1200px;
  height: auto;
}
/* Combine with srcset to serve the right source size */

7. Generating Too Many or Too Few Sizes

// TOO FEW: Big jump between sizes wastes bandwidth
const sizes = [400, 1600]; // Mobile gets 1600px image

// TOO MANY: Marginal benefit, increased complexity
const sizes = [300, 350, 400, 450, 500, 550, 600, 650, 700, 750, 800];

// JUST RIGHT: Cover key breakpoints with reasonable gaps
const sizes = [400, 800, 1200, 1600]; // ~2x gap between each

// For critical images (hero), add one more:
const heroSizes = [400, 800, 1200, 1600, 2400];

A good rule of thumb: each size should be roughly 1.5-2x the previous one.

Summary

Quick Reference: Image Sizing by Context

ContextWidthAspect RatioSizes to GenerateFormat Priority
Hero banner1200px base16:9400, 800, 1200, 1600, 2400AVIF > WebP > JPEG
Blog image800px base16:9 or 3:2400, 800, 1200AVIF > WebP > JPEG
Product main800px base1:1400, 800, 1600AVIF > WebP > JPEG
Product thumb400px base1:1200, 400AVIF > WebP > JPEG
Avatar96px base1:148, 96, 192AVIF > WebP > JPEG
OG image1200x6301.91:1Single sizeJPEG or PNG
Email image600px maxVaries600, 1200 (2x)JPEG or PNG

Resizing Strategy Checklist

  1. Start with a high-resolution original (at least 2x your largest display size)
  2. Choose the right aspect ratio for the context
  3. Generate sizes at 1.5-2x intervals covering your breakpoints
  4. Always set width and height attributes on <img> elements
  5. Use srcset with sizes for fluid images
  6. Use density descriptors (2x, 3x) only for fixed-size images
  7. Serve modern formats (AVIF, WebP) with JPEG/PNG fallback
  8. Never upscale - use withoutEnlargement: true in processing tools
  9. Use CDN-based resizing to avoid managing generated files
  10. Set performance budgets and monitor image sizes in CI

When to Use Each Approach

ApproachBest ForTradeoffs
CDN resizing (Sirv, Cloudinary)Most websitesOngoing cost, external dependency
Build-time (Astro, Vite)Static sites, blogsSlower builds, disk space
Upload-time (Sharp pipeline)User-generated contentServer compute, storage for variants
Client-side (Canvas)Upload previews, cropsOnly for UI, not delivery

For most projects, CDN-based resizing offers the best combination of simplicity, performance, and flexibility. Upload your originals once and let the CDN handle the rest. Sirv CDN provides automatic resizing, format conversion, and global delivery from a single URL.

Related Resources

Format References

Ready to optimize your images?

Sirv automatically optimizes, resizes, and converts your images. Try it free.

Start Free Trial