# Unoptimized images

> Images without dimensions shift layout; oversized ones waste bandwidth. Add width/height, lazy-load below the fold, and serve the right size.

_Category: Performance · Detector `unoptimized-images` · Severity: error_

Images are usually the heaviest thing on a page, and when they ship without `width`/`height` they shift the layout as they load, when they ship at 2400px in a 400px slot they waste most of what they download, and when they load eagerly below the fold they steal bandwidth from what’s actually visible. AI scaffolds paste raw `<img src>` tags with none of the attributes that make images fast.

## Symptoms

- Layout jumps as each image loads (no reserved space)
- A tiny thumbnail downloads a multi-megapixel original
- Below-the-fold images load immediately, delaying the hero
- Images look slightly stretched or squished (wrong declared ratio)

## How VibeCheck detects it

The `unoptimized-images` detector flags this live in the browser and reports it to the widget's Problems list — and to your coding agent over MCP.

- **Issue string:** `hero.png (2400×1200) missing width/height`
- **Threshold:** Natural size > 2× rendered size, or declared aspect ratio off by > 0.15, or missing dimensions/lazy/alt

## Root causes

- No `width`/`height` attributes, so the browser can’t reserve space
- The source image is far larger than the size it’s displayed at
- No `loading="lazy"` on images below the fold
- Declared `width`/`height` that don’t match the file’s real aspect ratio

## The fix

Give every image explicit intrinsic `width` and `height` so the box is reserved before load, add `loading="lazy"` to below-the-fold images, serve the image at roughly the size it’s displayed (2× max for retina) in a modern format (WebP/AVIF), and use `srcset`/`sizes` for responsive delivery.

### Steps

1. Add `width` and `height` attributes matching the file’s real aspect ratio
2. Add `loading="lazy"` to images below the fold (never to the LCP image)
3. Resize and re-encode to WebP/AVIF at the displayed size, with `srcset` for density

_Responsive, non-shifting, right-sized image_

```html
<img
  src="/hero-800.webp"
  srcset="/hero-800.webp 800w, /hero-1600.webp 1600w"
  sizes="(max-width: 600px) 100vw, 800px"
  width="800" height="450"
  loading="lazy" decoding="async"
  alt="Product dashboard" />
```

## Framework-specific fixes

### React

Plain React has no image pipeline, so add the attributes yourself — `width`/`height`, `loading`, `decoding` — and pre-generate responsive sizes at build time or via an image CDN.

```tsx
<img
  src="/hero-800.webp"
  width={800}
  height={450}
  loading="lazy"
  decoding="async"
  alt="Product dashboard"
/>
```

### Next.js

`next/image` does all of this for you: it reserves space from `width`/`height`, lazy-loads by default, generates WebP/AVIF and a `srcset`, and serves the right size per device. Set `priority` on the LCP image so it isn’t lazy-loaded.

```tsx
import Image from 'next/image'

<Image src="/hero.jpg" width={800} height={450} alt="Product dashboard" priority />
```

[Next.js docs](https://nextjs.org/docs/app/api-reference/components/image)

### Vue

Set the same attributes in the template. On Nuxt, use `<NuxtImg>` from `@nuxt/image` for automatic resizing and modern formats.

```vue
<template>
  <img src="/hero-800.webp" width="800" height="450" loading="lazy" alt="Product dashboard" />
  <!-- Nuxt: <NuxtImg src="/hero.jpg" width="800" height="450" loading="lazy" /> -->
</template>
```

[Vue docs](https://image.nuxt.com/)

### Svelte

`@sveltejs/enhanced-img` processes local images at build time — generating dimensions, modern formats, and a `srcset` from a single `<enhanced:img>` tag.

```svelte
<script>
  import hero from '$lib/hero.jpg?enhanced'
</script>

<enhanced:img src={hero} alt="Product dashboard" />
```

[Svelte docs](https://svelte.dev/docs/kit/images)

### Vanilla JS

Author the full attribute set by hand and pre-process images (squoosh, sharp, or a CDN) to the displayed size and a modern format.

```html
<img src="/hero-800.webp" width="800" height="450" loading="lazy" decoding="async" alt="Product dashboard" />
```

## FAQ

### Should I lazy-load every image?

No — never lazy-load your LCP (largest, above-the-fold) image; that delays the most important paint. Lazy-load only images below the fold. In Next.js, mark the hero with `priority`.

### What does "oversized" mean exactly?

VibeCheck flags an image whose natural (file) dimensions are more than twice its rendered (displayed) size — you’re downloading at least 4× the pixels you show. Resize to the displayed size, 2× for retina.

### Why does missing `width`/`height` cause layout shift?

Without dimensions the browser doesn’t know the image’s size until it downloads, so it reserves no space and everything below jumps when the image arrives. The attributes let it reserve the box up front.

## Related problems

- [Large image files](https://vibecheck.wcgw.fun/fix/large-image-files.md) — Performance
- [Cumulative layout shift (CLS)](https://vibecheck.wcgw.fun/fix/cumulative-layout-shift.md) — Performance
- [Images missing alt text](https://vibecheck.wcgw.fun/fix/missing-image-alt-text.md) — Search visibility
- [Large JavaScript bundles](https://vibecheck.wcgw.fun/fix/large-javascript-bundles.md) — Performance

---

Fix guide from VibeCheck — https://vibecheck.wcgw.fun/fix/unoptimized-images. Full site index for LLMs: https://vibecheck.wcgw.fun/llms.txt
