Performance18 min read

12 Image Optimization Mistakes That Slow Down Your Website

The 12 image mistakes that cost the most performance, why each one hurts, how to spot it in DevTools, and the exact fix. Covers oversized files, missing dimensions, lazy-loaded LCP images, and more.

By ImageGuide Team·Published August 15, 2026·Updated August 15, 2026
image optimizationweb performancecore web vitalslcpclsmistakes

Images are the largest part of most web pages, so image mistakes cost more than any other kind. The good news: the same twelve mistakes appear on almost every slow site, and every one of them has a known fix.

This list is ordered by impact. Each entry explains why the mistake hurts, how to find it on your own site, and what to change.

Quick Diagnosis

Run this in the browser console on any page. It reports every image that is served much larger than it is displayed.

[...document.querySelectorAll('img')]
  .map(img => ({
    src: img.currentSrc.split('/').pop(),
    natural: `${img.naturalWidth}x${img.naturalHeight}`,
    displayed: `${Math.round(img.getBoundingClientRect().width)}x${Math.round(img.getBoundingClientRect().height)}`,
    wasteFactor: +(img.naturalWidth / (img.getBoundingClientRect().width * devicePixelRatio)).toFixed(1),
    lazy: img.loading === 'lazy',
    hasSize: img.hasAttribute('width') && img.hasAttribute('height')
  }))
  .filter(r => r.wasteFactor > 1.5 || !r.hasSize)
  .sort((a, b) => b.wasteFactor - a.wasteFactor)
  .forEach(r => console.table([r]));

A wasteFactor above 2 means you ship at least four times more pixels than the screen can use. Keep the output open as you read the list.

1. Serving Full-Resolution Originals

This is the single most expensive mistake on the web. A camera or phone produces a 4000×3000 image. It gets uploaded, and CSS scales it down to a 400-pixel-wide thumbnail. The browser still downloads all twelve megapixels.

Why It Hurts

Decoding cost grows with pixel count, not with display size. An oversized image blocks the main thread while it decodes, delays Largest Contentful Paint, and burns mobile data.

Source image File size Displayed at Wasted bytes
4000×3000 JPEG q90 3,900 KB 400×300 ~3,860 KB
2400×1600 JPEG q85 1,150 KB 600×400 ~1,050 KB
1200×800 JPEG q82 210 KB 600×400 ~150 KB
800×533 JPEG q82 96 KB 600×400 ~0 KB

Bytes wasted by serving originals

Share of the downloaded file that the browser throws away after scaling. Based on the table above.

How to Spot It

In Chrome DevTools, open the Network tab, filter to Img, and add the Dimensions column. Compare each row against the rendered size in the Elements panel. Lighthouse reports the same issue as “Properly size images”.

The Fix

Generate a set of widths and let the browser choose:

<img
  src="product-800.jpg"
  srcset="product-400.jpg 400w,
          product-800.jpg 800w,
          product-1200.jpg 1200w,
          product-1600.jpg 1600w"
  sizes="(max-width: 640px) 100vw, 600px"
  width="800"
  height="533"
  alt="Walnut desk lamp with brass shade">

If you do not want to maintain four files per image, an image CDN generates them on request. With Sirv, the width is a URL parameter:

<img
  src="https://demo.sirv.com/product.jpg?w=800"
  srcset="https://demo.sirv.com/product.jpg?w=400 400w,
          https://demo.sirv.com/product.jpg?w=800 800w,
          https://demo.sirv.com/product.jpg?w=1200 1200w"
  sizes="(max-width: 640px) 100vw, 600px"
  width="800"
  height="533"
  alt="Walnut desk lamp with brass shade">

Read the image resizing guide for the full sizing method.

2. Shipping Only JPEG and PNG

WebP has been supported by every major browser since 2021. AVIF has been supported by every major browser since Safari 16 in 2022. A site that still serves only JPEG and PNG pays a 25% to 50% size penalty on every image request.

Typical Savings

Content JPEG q82 WebP q80 AVIF q60
Photograph, 1200×800 210 KB 148 KB 96 KB
Product on white, 1000×1000 145 KB 92 KB 58 KB
Screenshot with text, 1440×900 380 KB (PNG: 620 KB) 210 KB 165 KB
Flat illustration, 1000×1000 PNG: 180 KB 44 KB 31 KB

The Fix

Use <picture> so unsupported browsers fall back cleanly:

<picture>
  <source srcset="hero.avif" type="image/avif">
  <source srcset="hero.webp" type="image/webp">
  <img src="hero.jpg" width="1200" height="630" alt="Team working at a shared desk">
</picture>

Order matters. The browser takes the first <source> it understands, so list the smallest format first.

An image CDN removes the markup entirely. It reads the Accept header and returns AVIF, WebP, or JPEG per request from a single URL. See the WebP guide and the AVIF guide for encoder settings.

3. Lazy Loading the LCP Image

loading="lazy" on the hero image is the most common self-inflicted performance wound. It is easy to make: a developer adds the attribute to every <img> in a template, including the one at the top of the page.

Why It Hurts

A lazy image is not fetched until layout runs and the browser knows it is in the viewport. That adds a full round trip to the critical path. LCP regressions of 500 ms to 1,500 ms from this one attribute are routine.

How to Spot It

new PerformanceObserver(list => {
  const lcp = list.getEntries().at(-1);
  console.log('LCP element:', lcp.element);
  console.log('loading attr:', lcp.element?.getAttribute('loading'));
  console.log('LCP time:', Math.round(lcp.startTime), 'ms');
}).observe({ type: 'largest-contentful-paint', buffered: true });

If loading attr prints lazy, you found it.

The Fix

Image position loading fetchpriority
LCP / hero eager (or omit) high
Above the fold, not LCP eager auto
Below the fold lazy auto
Decorative, below fold lazy low
<!-- Hero: never lazy -->
<img src="hero.jpg" fetchpriority="high" width="1200" height="630" alt="…">

<!-- Everything below the fold -->
<img src="section-3.jpg" loading="lazy" width="800" height="600" alt="…">

The lazy loading guide covers the threshold rules in detail.

4. Missing Width and Height Attributes

Without intrinsic dimensions, the browser reserves no space for an image. When the image arrives, everything below it jumps. That is Cumulative Layout Shift, and it is the easiest Core Web Vital to fix.

The Fix

Always set width and height to the image’s real pixel dimensions, then let CSS control the display size:

<img src="photo.jpg" width="1200" height="800" alt="…">
img {
  max-width: 100%;
  height: auto; /* preserves the aspect ratio from the attributes */
}

The attributes are not a display size. They are a ratio hint. Modern browsers compute aspect-ratio: 1200 / 800 from them and reserve the box before a single byte arrives.

For images whose ratio is unknown at build time, such as user uploads, set the ratio on the container:

.upload-slot {
  aspect-ratio: 4 / 3;
  background: #f1f1f3;
}
.upload-slot img {
  width: 100%;
  height: 100%;
  object-fit: cover;
}

5. Using PNG for Photographs

PNG is lossless. On a photograph, that means it stores sensor noise faithfully at enormous cost. A photo saved as PNG is commonly five to ten times larger than the same photo as a well-tuned JPEG that nobody can distinguish from it.

Image type Correct format Wrong choice Penalty
Photograph JPEG / WebP / AVIF PNG 5–10×
Logo, icon, line art SVG PNG at 3 sizes 10–50×
Screenshot with text PNG / WebP lossless JPEG Text fringing
Flat illustration, few colours SVG or PNG-8 PNG-24 2–4×
Photo needing transparency WebP / AVIF PNG-24 3–8×
Animation MP4 / WebM / animated WebP GIF 5–20×

The one case where PNG is still right for photographic content is a photo that needs a real alpha channel and must work in a very old client. On the web, WebP and AVIF both support alpha and both beat PNG heavily. See the PNG optimization guide.

6. One Image for Every Screen

A single 1600-pixel-wide file sent to a 375-pixel phone is four times too wide. A single 800-pixel file stretched across a 27-inch monitor looks soft. Both problems come from the same cause: no srcset.

The Fix

Two attributes carry the whole responsive image system:

<img
  src="banner-1200.jpg"
  srcset="banner-600.jpg 600w,
          banner-900.jpg 900w,
          banner-1200.jpg 1200w,
          banner-1800.jpg 1800w"
  sizes="(max-width: 700px) 100vw,
         (max-width: 1100px) 50vw,
         640px"
  width="1200"
  height="500"
  alt="…">

srcset lists what exists. sizes tells the browser how wide the image will render, before CSS has loaded. Getting sizes wrong is worse than omitting srcset, because the browser trusts it. Verify it with img.currentSrc at several viewport widths.

Choosing Breakpoints

A practical ladder that covers nearly all devices without generating dozens of files:

Width Serves
400w Small phones, thumbnails
800w Phones at 2× DPR, tablets
1200w Laptops, tablets at 2× DPR
1600w Desktop, laptops at 2× DPR
2400w Large or high-DPI desktop only

The responsive images guide explains the descriptor rules fully.

7. Leaving Camera Metadata in Place

A JPEG straight from a phone carries EXIF data, GPS coordinates, a thumbnail preview, and often an embedded colour profile. That is typically 20 KB to 100 KB per file that no visitor ever sees.

Two Problems in One

The first is weight. On a gallery of 60 images, 50 KB of metadata each is 3 MB of pure overhead.

The second is privacy. GPS coordinates in a user-uploaded photo expose the location where it was taken. If your site accepts uploads, stripping metadata is a safety requirement, not an optimization.

The Fix

# Strip everything except the colour profile
mogrify -strip -define png:exclude-chunk=all *.jpg

# Sharp (Node)
sharp(input)
  .withMetadata({ icc: 'srgb' })  // keeps sRGB, drops the rest
  .jpeg({ quality: 82 })
  .toFile(output);

# ExifTool, recursive, keeps orientation
exiftool -all= -tagsFromFile @ -Orientation -r -overwrite_original ./images

Keep the orientation tag or your portrait photos will appear sideways. Keep the ICC profile only if the image is not already sRGB. See the metadata and privacy guide.

8. Animated GIFs Instead of Video

GIF is a 1987 format limited to 256 colours and frame-based compression with no motion prediction. A three-second screen recording is 4 MB as a GIF and 180 KB as an MP4.

Clip GIF Animated WebP MP4 (H.264)
3 s UI recording, 800×600 4,100 KB 720 KB 180 KB
2 s product spin, 600×600 2,400 KB 480 KB 120 KB
5 s looping banner, 1200×400 6,800 KB 1,100 KB 260 KB

The Fix

<video autoplay loop muted playsinline width="800" height="600" poster="preview.jpg">
  <source src="demo.webm" type="video/webm">
  <source src="demo.mp4" type="video/mp4">
</video>

muted and playsinline are both required for autoplay on iOS. The poster frame prevents an empty box while the video loads.

ffmpeg -i animation.gif -movflags faststart -pix_fmt yuv420p \
  -vf "scale=trunc(iw/2)*2:trunc(ih/2)*2" demo.mp4

The GIF migration guide covers the full conversion workflow.

9. Exporting at Quality 100

Quality 100 does not mean “perfect”. In JPEG it means the encoder skips almost all quantization, producing a file two to three times larger than quality 85 with no difference a human eye can see at normal viewing distance.

Quality File size Visible difference
100 640 KB Baseline
95 380 KB None
90 265 KB None
85 195 KB None on photos
80 158 KB None on most photos
75 130 KB Slight on flat gradients
60 88 KB Visible on skies, skin

Measured on a 1600×1067 landscape photograph.

The Fix

Set defaults per format and per content type:

Format Photos Product on white Screenshots
JPEG 80–85 85–90 Use PNG or WebP lossless
WebP 75–82 80–85 lossless: true
AVIF 50–63 60–68 65–75

AVIF quality numbers are not comparable to JPEG numbers. AVIF quality 60 is roughly JPEG quality 85. Do not copy your JPEG setting across.

10. Serving Images From Your Origin Server

Every image request that reaches your application server is a request that could have been answered from a cache edge 20 ms from the visitor. Origin delivery adds latency for distant users, consumes application capacity, and rules out per-request format negotiation.

What a CDN Changes

Concern Origin Image CDN
Latency for distant users 200–800 ms 20–60 ms
Format negotiation Manual <picture> Automatic per request
Resizing Build step or upload step URL parameter
Cache invalidation Your problem Managed
Origin load Full Near zero after first request

Sirv handles resizing, format negotiation, and global delivery from one URL. The image CDN comparison evaluates the alternatives side by side.

Cache Headers Still Matter

A CDN cannot help if the origin sends no cache headers. Version your filenames and cache aggressively:

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

11. Hiding the Hero Image Behind JavaScript

A carousel that renders client-side, an image inside a hydrated React component, or a data-src swapped by a script all share one problem: the browser cannot discover the image in the HTML. The preload scanner finds nothing, and the fetch starts only after JavaScript executes.

Why It Hurts

The preload scanner is the browser’s biggest performance advantage. It reads ahead in the raw HTML and starts fetches while the parser is still blocked. JavaScript-injected images bypass it completely, adding the entire script download, parse, and execute time to the LCP path.

How to Spot It

Disable JavaScript in DevTools and reload. If the hero image disappears, the preload scanner never sees it either.

The Fix

Put the first slide in the HTML as a plain <img> and let JavaScript enhance it afterwards:

<div class="carousel" data-carousel>
  <!-- Slide 1 is real HTML: discoverable, preloadable -->
  <img src="slide-1.jpg" width="1200" height="600" fetchpriority="high" alt="…">
  <!-- Slides 2+ injected by script after load -->
</div>

If the image genuinely cannot be in the initial HTML, preload it:

<link rel="preload" as="image" href="hero.avif" type="image/avif" fetchpriority="high">

See the preloading and priority hints guide.

12. Optimizing Once and Never Measuring Again

Image performance decays. A designer uploads a 6 MB PNG to the CMS. A new marketing section ships with unoptimized assets. Six months after the audit, the page weighs more than it did before.

The Fix: An Image Budget in CI

Fail the build when images exceed a threshold:

// scripts/check-image-budget.mjs
import { readdir, stat } from 'node:fs/promises';
import { join, extname } from 'node:path';

const LIMITS = { '.jpg': 250_000, '.jpeg': 250_000, '.png': 150_000, '.webp': 200_000, '.avif': 150_000, '.gif': 100_000 };
const DIR = 'public/images';

const walk = async (dir) => {
  const entries = await readdir(dir, { withFileTypes: true });
  const files = await Promise.all(entries.map(async (e) => {
    const p = join(dir, e.name);
    return e.isDirectory() ? walk(p) : [p];
  }));
  return files.flat();
};

const violations = [];
for (const file of await walk(DIR)) {
  const limit = LIMITS[extname(file).toLowerCase()];
  if (!limit) continue;
  const { size } = await stat(file);
  if (size > limit) violations.push(`${file}: ${Math.round(size / 1024)} KB (limit ${Math.round(limit / 1024)} KB)`);
}

if (violations.length) {
  console.error('Image budget exceeded:\n' + violations.join('\n'));
  process.exit(1);
}
console.log('Image budget OK');

Pair it with a Lighthouse CI assertion so regressions in LCP surface on the pull request that caused them. The CI/CD pipelines guide has ready-to-use workflow files.

Summary

The Twelve at a Glance

# Mistake Primary cost Fix
1 Full-resolution originals LCP, bandwidth srcset + real widths
2 JPEG and PNG only 25–50% extra bytes AVIF and WebP with fallback
3 Lazy-loaded LCP image LCP +0.5–1.5 s loading="eager", fetchpriority="high"
4 No width/height CLS Set intrinsic dimensions
5 PNG for photos 5–10× size JPEG, WebP, or AVIF
6 One size for all screens Mobile waste, blurry desktop srcset + accurate sizes
7 Camera metadata retained 20–100 KB per file, privacy Strip EXIF, keep orientation
8 Animated GIF 5–20× size MP4 or WebM video
9 Quality 100 2–3× size 80–85 JPEG, 50–63 AVIF
10 Origin delivery Latency, no negotiation Image CDN
11 JS-injected hero Preload scanner blind Real <img> in HTML
12 No ongoing measurement Slow decay Image budget in CI

Checklist

  1. ✅ No image is served more than 1.5× its displayed size at the target DPR
  2. ✅ AVIF or WebP is offered on every raster image
  3. ✅ The LCP image is not lazy loaded and carries fetchpriority="high"
  4. ✅ Every <img> has width and height
  5. ✅ Format matches content type (photo, art, screenshot, animation)
  6. srcset and sizes are present and verified with currentSrc
  7. ✅ EXIF and GPS data are stripped from uploads
  8. ✅ No animated GIFs remain in production
  9. ✅ Quality settings are tuned per format, not left at 100
  10. ✅ Images are delivered from a CDN with long cache lifetimes
  11. ✅ The hero image exists in the initial HTML
  12. ✅ CI fails when an image exceeds the budget

Fix the first three and most sites see the largest single improvement they will ever get from a front-end change. Work down the rest in order, then put mistake 12 in place so the gains hold.

Related Resources

Format References

Ready to optimize your images?

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

Start Free Trial