Image Optimization for Web: Complete Performance Guide

Why Image Optimization Matters

Images account for roughly half of the average web page's weight. They are also the most common **Largest Contentful Paint** element on a page. If your images are slow, your Core Web Vitals are slow, your SEO suffers, and users leave before the page finishes loading.

The math is brutal. A page with five unoptimized hero photos can easily weigh 5 MB. On a typical 4G mobile connection that is several seconds of waiting before anything meaningful appears. Cut image weight by 70 percent and you cut seconds off your load time with no other changes.

This guide gives you a repeatable workflow. You can compress every image in it using the [image compressor](/en/image-compressor) directly in the browser.

The Goal: Right Size, Right Format, Right Delivery

Three questions define a well-optimized image:

1. **Right size.** Are you delivering the pixels the device actually needs?

2. **Right format.** Are you using the most efficient codec for the content?

3. **Right delivery.** Are you lazy loading, caching, and serving from a CDN?

Get all three right and your images will be small, sharp, and fast. Get any one wrong and the others barely matter.

Lossy vs Lossless Compression

**Lossless** compression preserves every pixel exactly. The file can be decoded back to the identical source. Use it when precision matters: logos, screenshots with text, medical or scientific images, and source-of-truth archives.

**Lossy** compression discards visual information that the human eye tolerates losing. The file cannot be decoded back to the original, but it is dramatically smaller at the same perceived quality. Use it for photos and complex imagery on the web.

For web delivery, **lossy is almost always correct for photographic content**. Users do not compare your hero image to a reference; they notice if the page is slow. Tune quality to the point where artifacts are not visible and stop there.

Modern lossy formats are remarkable. AVIF and WebP at sensible quality settings often beat the visual quality of older JPEGs while weighing half as much. See our [PNG vs WebP vs AVIF](/en/blog/png-vs-webp-vs-avif-image-format-guide) comparison for the full breakdown.

Pick the Right Format

A simple decision tree:

  • **Photos and complex imagery.** Serve AVIF first, WebP second, JPEG fallback.
  • **Logos, icons, screenshots with text or sharp edges.** PNG, or lossless WebP/AVIF.
  • **Simple icons and UI art.** SVG, inline where possible.
  • **Animation.** WebP or AVIF animation; avoid GIF.

Serve modern formats with the `<picture>` element so the browser picks the best it supports:


<picture>
  <source srcset="hero.avif" type="image/avif">
  <source srcset="hero.webp" type="image/webp">
  <img src="hero.jpg" alt="..." width="1920" height="1080" loading="lazy">
</picture>

Always include a JPEG or PNG fallback as the final `<img>`.

Serve the Right Size: Responsive Images

Sending a 4000-pixel-wide photo to a 375-pixel phone is wasteful. The browser downloads bytes it will never display and then shrinks the image, burning CPU.

Use `srcset` and `sizes` to let the browser choose:


<img
  src="hero-1600.jpg"
  srcset="hero-480.jpg 480w, hero-800.jpg 800w, hero-1200.jpg 1200w, hero-1600.jpg 1600w"
  sizes="(max-width: 600px) 100vw, (max-width: 1200px) 50vw, 800px"
  alt="..."
  width="1600" height="900" loading="lazy">

A practical set of widths for most projects: 480, 800, 1200, 1600, and 2400. That covers phones, tablets, laptops, and large displays without exploding your build pipeline.

**Always set width and height.** Even with `srcset`, the browser needs the aspect ratio to reserve space and avoid Cumulative Layout Shift. The displayed size can differ from the intrinsic size; what matters is that the ratio is known up front.

Lazy Loading

Most images below the fold should load only when the user scrolls near them. The native attribute is enough for the vast majority of cases:


<img src="..." loading="lazy" decoding="async" width="..." height="..." alt="...">

Rules of thumb:

  • **Lazy load** everything below the fold.
  • **Eager load** the LCP image (usually the hero) and any image in the first viewport. Add `fetchpriority="high"` to the LCP image.
  • Use `decoding="async"` so decoding does not block the main thread.

For the hero, consider a low-quality placeholder or a blur-up technique so the user sees something immediately, then the full image sharpens when it loads.

Compression Tools Compared

ToolStrengthsBest for
In-browser [image compressor](/en/image-compressor)No upload, fast, multi-format, side-by-side qualityOne-offs and small batches
`sharp` (Node)Fast, scriptable, programmaticBuild pipelines
`cwebp` / `avifenc`Fine control over quality and effortCLI workflows
SquooshVisual quality comparisonTuning quality for one image
ImageMagickBroad format supportBatch scripting
CDN transforms (Cloudflare, Cloudinary, Imgix, Fastly)Automatic format negotiation, on-the-fly resizeProduction at scale

For most teams, the right combination is: a build step with `sharp` to generate `srcset` variants and modern formats, plus a CDN that re-optimizes and serves AVIF or WebP based on the `Accept` header. For quick fixes and one-off images, the [image compressor](/en/image-compressor) does both compression and conversion without uploading.

Core Web Vitals and Images

Three Core Web Vitals intersect with images:

**Largest Contentful Paint (LCP).** The largest visible element, often a hero image, should render within 2.5 seconds. Slow images blow this budget. Fix with the right format, the right size, lazy loading of non-LCP images, priority hints on the LCP image, and a CDN close to the user.

**Cumulative Layout Shift (CLS).** Images without explicit dimensions push content around as they load. Fix by always setting `width` and `height` (or `aspect-ratio` in CSS) so the browser reserves space.

**Interaction to Next Paint (INP).** Heavy image decoding on the main thread delays interaction. Fix with `decoding="async"`, smaller files, and avoiding enormous images that the browser must downscale at runtime.

Track these in the field with Chrome UX Report, PageSpeed Insights, and your RUM tooling. Lab tools like Lighthouse are useful for finding problems, but field data is what Google uses for ranking signals.

Before and After: A Real Example

A product page ships a 4000x2667 hero photo as a 1.8 MB JPEG with no `srcset` and no lazy loading on below-the-fold gallery images.

Optimization pass:

1. Compress and convert the hero to AVIF with WebP and JPEG fallbacks: 1.8 MB becomes ~210 KB (AVIF), ~290 KB (WebP), ~420 KB (JPEG).

2. Generate 480, 800, 1200, 1600, and 2400 widths; add `srcset` and `sizes`.

3. Add `width` and `height` to every image.

4. Lazy load gallery images; add `fetchpriority="high"` to the hero.

5. Serve through a CDN with edge caching.

Result: total image weight drops from roughly 5 MB to under 700 KB. LCP moves from 4.2 seconds to 1.6 seconds on a mid-tier Android over 4G. CLS goes to near zero. The page feels instant.

This is not unusual. Most unoptimized sites can cut image weight by 70 to 85 percent with a disciplined pass.

A Repeatable Workflow

Run this on every image you publish:

1. **Start from the source.** Always re-encode from the original; never re-compress an already-compressed file.

2. **Resize to the largest display size you need**, plus a small margin for retina. Do not ship a 4000px image for a 800px slot.

3. **Compress and convert.** Produce AVIF, WebP, and a JPEG or PNG fallback at each width. Use the [image compressor](/en/image-compressor) for one-offs.

4. **Generate `srcset` variants** for responsive delivery.

5. **Add attributes.** `width`, `height`, `alt`, `loading` (lazy for below the fold, eager for LCP), `decoding="async"`, and `fetchpriority` where relevant.

6. **Use `<picture>`** to serve the best format each browser supports.

7. **Cache and CDN.** Set long `Cache-Control` with a hashed filename for cache busting, and serve from a CDN close to users.

8. **Measure.** Run Lighthouse and check field CWV data. Fix regressions.

If your CMS or framework can automate steps 2 through 6, do it. Manual image optimization does not scale.

CDN and Caching Considerations

A few rules:

  • **Cache aggressively.** Images are immutable; use `Cache-Control: public, max-age=31536000, immutable` with content-hashed filenames so you can bust the cache by changing the name.
  • **Negotiate format at the edge.** Let the CDN look at the `Accept` header and serve AVIF or WebP automatically.
  • **Resize at the edge.** Many CDNs can generate variants on demand via URL params, which removes the need to pre-generate every size.
  • **Watch the variants explosion.** If you produce 5 widths times 3 formats times 2 resolutions, you have 30 files per image. A CDN that transforms on demand can keep your storage simpler.
  • **Compress before you upload.** Even with a CDN, smaller originals mean lower storage and faster transforms.

Accessibility and SEO

Optimization is not just performance.

  • **Alt text.** Every meaningful image gets descriptive alt text. Decorative images get empty `alt=""` so screen readers skip them.
  • **Filename.** A descriptive, keyword-relevant filename helps image search.
  • **Structured data.** For product images, recipes, and articles, add the relevant Schema.org image properties.
  • **Captions.** Where appropriate, captions improve comprehension and engagement.

Common Mistakes

  • **Serving a single huge image to every device.**
  • **Forgetting width and height, causing layout shift.**
  • **Lazy loading the LCP image.**
  • **Re-compressing already-compressed images.**
  • **Skipping the fallback for modern formats.**
  • **Using GIF for animation.** WebP or AVIF is far smaller.
  • **Leaving cache headers short.** Immutable images should be cached for a year.
  • **Optimizing only the hero.** Below-the-fold images add up; lazy loading helps but does not excuse a 4 MB gallery.

TL;DR

Cut image weight by 70 percent or more with a disciplined workflow: right size, right format, right delivery. Compress and convert in the [image compressor](/en/image-compressor), serve AVIF with WebP and PNG/JPEG fallbacks via `<picture>`, use `srcset` for responsive sizes, set `width` and `height`, lazy load below the fold, eager load the LCP, and cache aggressively behind a CDN. Measure with Lighthouse and field Core Web Vitals, then iterate.