A common image performance problem is surprisingly simple: a browser downloads a 3000-pixel image, only to display it inside a 350-pixel-wide card.

The image looks fine. The layout works. But the browser is transferring far more data than it needs.

When optimizing images, I find it more useful to think about three things in order:

  1. Dimensions: How large does the image actually need to be?
  2. Format: Which format makes sense for this type of content?
  3. Quality: How much compression can you apply without affecting the result?

Let's walk through a small example using Node.js and Sharp.

1. Start with dimensions

Suppose you have a product image that's 3000 × 2000 pixels.

Your website displays it in a card that's approximately 350 pixels wide.

You probably don't need to serve the original file.

Depending on the layout and device pixel density, generating 640px and 1200px versions could be a reasonable starting point.

The exact sizes should come from your layout, not from a fixed rule.

One important detail: CSS resizing does not reduce the file the browser downloads. You need appropriately sized image assets.

2. Choose the right format

Different formats have different strengths.

Format Good for
JPEG Photographs and broad compatibility
WebP Most modern website images
AVIF Potentially smaller image transfers
PNG Lossless graphics and pixel-precise assets
SVG Icons and vector illustrations

For product photography, I usually start by comparing JPEG and WebP.

AVIF is also worth testing, but don't assume it always produces the smallest acceptable file. Results depend on the image and encoder settings.

The goal isn't to use the newest format. It's to deliver an image that looks good without transferring unnecessary bytes.

3. Generate optimized images with Sharp

Sharp is a popular image-processing library for Node.js.

Install it:

npm install sharp 

Put a source image named photo.jpg in your project directory. For this example, use a source at least 1200 pixels wide.

Create optimize.mjs:

import sharp from "sharp"; import { stat } from "node:fs/promises"; const input = "photo.jpg"; const widths = [640, 1200]; for (const width of widths) { const output = `photo-${width}.webp`; await sharp(input) .rotate() .resize({ width, withoutEnlargement: true, }) .webp({ quality: 78 }) .toFile(output); const { size } = await stat(output); console.log( `${output}: ${(size / 1024).toFixed(1)} KB` ); } 

Run it:

node optimize.mjs 

You should now have two WebP images.

Open both and compare them with the original.

Pay attention to small text, edges, texture, and areas containing subtle color variations.

A quality setting of 78 is just a starting point. It isn't a universal recommendation.

4. Serve the correct image

Creating smaller files is only half the job.

Your browser also needs to know which size to download.

For example:

<img src="/images/photo-1200.webp" srcset=" /images/photo-640.webp 640w, /images/photo-1200.webp 1200w " sizes="(max-width: 720px) 100vw, 720px" width="1200" height="800" alt="Product photograph" loading="lazy" /> 

This example assumes a 3:2 source image and that the generated assets are available under /images/.

The srcset attribute provides image alternatives. The sizes attribute tells the browser approximately how much space the image will occupy.

The browser can then select an appropriate source.

Also, don't automatically lazy-load your main hero image. Images visible immediately when the page loads may need to load promptly.

5. Measure the result

After optimization, open Chrome DevTools.

Check:

  • Which image file was requested?
  • How many bytes were transferred?
  • Did the mobile viewport select the expected source?
  • Does the image still look good at its actual display size?

Then check the page's performance.

A smaller image may help, but optimizing one file doesn't automatically fix slow page loading. Server response times, JavaScript, CSS, and the position of your main image also matter.

6. When should image processing move to an API?

A small Sharp script works well when you control a limited set of images.

Things become different when an application handles user uploads, product catalogs, multiple output formats, or dynamically requested dimensions.

In those situations, a reusable image-processing API can keep transformation rules consistent across applications.

This is one of the workflows we're working on with Imazeo, alongside PDF processing and other media tools.

Regardless of whether you use Sharp directly or an API, the same principle applies:

Start with the actual display requirements, generate suitable assets, and measure what the browser receives.

Final thought

Before optimizing an entire image library, pick one real page and test a single image.

Resize it, compare formats, inspect the quality, and check the browser's network requests.

That small experiment will often tell you more than following a generic compression checklist.

Originally published on the Imazeo Blog.