# Image sizes that do not hurt — a practical guide

> Images are most of the bytes on most pages. Find the 2 MB hero, the 1600-pixel thumbnail and the images with no width and height, then fix them with the right export size, format and attributes, on any CMS.

Updated 2026-09-25 · Speed & Core Web Vitals · HTML version: https://getreport.app/guides/image-sizes-that-do-not-hurt

One photo uploaded straight from a phone weighs more than the rest of the page together, and it is often the largest thing on screen, so it alone decides the Largest Contentful Paint. The fixes are mechanical once you can see the numbers: file size, real pixel size, displayed size, format, and whether the layout reserved space. This guide shows how to get those numbers for every image on a page and what to do with each row.

## Quick answer

- Export images at **the width they are displayed, times two** for high-density screens. A 400-pixel thumbnail needs an 800-pixel file at most.
- Save as **WebP** (or AVIF) at about 80 % quality; keep PNG only for screenshots with text and for transparency.
- Aim for **under 100 KB** for photos, **under 300 KB** for a full-width hero; a single image above 300 KB is the first thing to fix.
- Add **`width` and `height`** to every `<img>`. CSS can still scale it; the attributes let the browser reserve space.
- Add **`loading="lazy"`** to everything below the fold, and *never* to the hero.
- Run the [image size checker](https://getreport.app/tools/image-size-checker) to see all of it per image.

## Why image sizes matter

Bytes cost time, and on a phone on a 4G connection a 1.8 MB hero image takes two to four seconds on its own. Google's Largest Contentful Paint threshold is 2.5 s, so that one file can fail the page by itself. Multiply by a gallery of six and the page is unusable on mobile data.

The second problem is layout. When an `<img>` has no `width` and `height`, the browser does not know how tall it will be until the file arrives, so it lays the text out, then pushes it down when the image loads. That jump is what Cumulative Layout Shift measures, and images without dimensions are its most common cause.

The third is waste: a 1600-pixel photo shown in a 300-pixel slot downloads 28 times more pixels than the screen can show. The browser throws them away after decoding them, which costs CPU too.

## How getReport checks it

> **Free tool:** [Image size checker](https://getreport.app/tools/image-size-checker): List every image on a page with its file size, format, real pixel dimensions and the size it is displayed at. Find the 2 MB hero, the 1600-pixel thumbnail and the images with no width and height.

The report reads every `<img>` on the page (including `data-src` and the first `srcset` candidate that lazy-load plugins use), requests the first 64 KB of each file to read its format and real pixel dimensions, and takes the file size from the `Content-Length` header, so nothing large is downloaded twice. Together with the `width`, `height`, `loading` and `srcset` attributes from the HTML, that is enough for four findings:

> **Check: Heavy image files.** A single image over a few hundred kilobytes takes longer to download on mobile than the rest of the page. Uploading the original camera file is the usual cause.
>
> 1. Export each listed image at the size it is displayed and save it as WebP or AVIF at about 80 % quality.
> 2. Let your CMS or CDN resize on upload so this does not come back with the next photo.

> **Check: Every sized image is served close to its displayed size.** Sending a 1600-pixel photo for a 300-pixel slot downloads about 28 times more pixels than needed. The browser scales it down and the visitor pays for the difference.
>
> 1. Resize the listed images to the largest size they are shown at (twice that for high-density screens).
> 2. Use srcset with several sizes so phones get a smaller file than desktops.

> **Check: Every image declares width and height.** Without width and height the browser cannot reserve space, so the text jumps when each image arrives. That is the most common cause of a poor Cumulative Layout Shift score.
>
> 1. Add width and height attributes with the image's real proportions; CSS can still scale it.
> 2. In WordPress, recent versions add them automatically for images inserted through the editor; hard-coded theme images need them by hand.

> **Check: Broken images.** A broken image shows a placeholder or nothing at all, and every visitor sees it. Moved uploads, renamed files and expired CDN links are the usual causes.
>
> 1. Fix or replace the src of each listed image; re-upload files that were deleted.
> 2. After a migration, check the uploads folder path and any image CDN rewrite rule.

The table lists each image sorted by size with format, real pixels, displayed size (from the attributes), loading attribute and status:

![Images table: a 1.8 MB PNG hero at 1200×520 displayed at 300×130 in red, three 800×800 thumbnails displayed at 120×120, a 1600×900 photo with no dimensions, and one image answering 404](https://getreport.app/guides/img/image-sizes-that-do-not-hurt/images-table.webp "Every image with its file size, format, real pixels, displayed size and loading attribute. Oversized and heavy files are highlighted.")

The Chromium run adds the total image weight including CSS backgrounds (`image-count-and-weight`), and the speed module adds Lighthouse's format and sizing estimates, which count the bytes you would save.

## Step by step

### 1. Sort by size and fix the top three

The table is sorted by file size. The top three rows are usually 80 % of the image bytes. Fix those before anything else; the long tail of 20 KB icons does not matter.

### 2. Compare "Pixels" with "Displayed"

A row that says 1600×900 pixels, displayed 300×169, is serving five times too wide. Export at 600 pixels wide (twice the display width) and the file drops by about 85 %. If the displayed column says "not set", the page has no `width`/`height` attributes; measure the slot in the browser's dev tools (right-click → Inspect, hover the image) and export for that.

### 3. Choose the format

| Content | Format | Why |
| --- | --- | --- |
| Photos | WebP (AVIF if your pipeline supports it) | 25–50 % smaller than JPEG at the same quality; universal browser support |
| Screenshots, UI, text | PNG or WebP lossless | JPEG blurs text; WebP lossless is smaller than PNG |
| Logos, icons | SVG | Any size, tiny file, sharp on every screen |
| Animations | Video (`<video muted autoplay loop>`) | A GIF of a few seconds is often 5–20 MB; the same as MP4 is 200 KB |

Quality 80 in WebP is indistinguishable from the original for photos at normal viewing sizes; quality 90+ is rarely worth the bytes.

### 4. Add width and height

The attributes say the image's proportions, not necessarily the display size; CSS `width: 100%; height: auto` still scales it. What matters is the ratio, because the browser uses it to reserve the space.

```html
<!-- Reserve space: the ratio 3:2 comes from the attributes, CSS sets the display size -->
<img src="https://getreport.app/img/runner-x-800.webp" alt="Blue running shoe, side view"
     width="800" height="533" loading="lazy" decoding="async">
```

```css
img { max-width: 100%; height: auto; }
```

### 5. Serve several sizes with srcset

One file for every screen means phones download the desktop size. `srcset` lists the candidates and `sizes` tells the browser how wide the slot is at each breakpoint:

```html
<img src="https://getreport.app/img/runner-x-800.webp"
     srcset="/img/runner-x-400.webp 400w,
             /img/runner-x-800.webp 800w,
             /img/runner-x-1200.webp 1200w"
     sizes="(max-width: 640px) 100vw, 400px"
     width="800" height="533" alt="Blue running shoe, side view" loading="lazy">
```

The browser picks the smallest file that covers the slot at its pixel density. Most CMSs generate the sizes on upload; the theme has to print the attributes.

### 6. Lazy-load below the fold only

`loading="lazy"` delays the download until the image is near the viewport. Put it on everything below the first screen. Do not put it on the hero or the first product image: a lazy hero waits for the layout before it even starts downloading, which is the single most common self-inflicted LCP problem. See [Lazy loading done right](https://getreport.app/guides/lazy-loading-done-right).

### 7. Fix broken images

A 404 in the table means the file moved or was deleted. Every visitor sees a broken icon, and Google Images has nothing to index. Re-upload it, fix the path (a common one after a migration is the uploads folder or a CDN rewrite rule), or remove the tag.

## Platform notes

**WordPress.** Media → uploads are resized on upload into the sizes registered by the theme; use the size that matches the slot (Add media → Image size) rather than "Full". Recent versions add `width`, `height` and `loading="lazy"` automatically for images inserted through the editor; theme images and page-builder widgets often do not. For conversion, a plugin such as ShortPixel, EWWW or Smush converts existing uploads to WebP and serves them through `<picture>` or rewrite rules; Performance Lab (from the WordPress team) converts on upload. Keep one such plugin, not three.

**Shopify.** The `image_url` filter with `width:` and the `image_tag` filter print `srcset`, `width` and `height` correctly in Liquid:

```liquid
{{ product.featured_image | image_url: width: 800 | image_tag: widths: '400, 800, 1200', sizes: '(max-width: 640px) 100vw, 400px', loading: 'lazy' }}
```

Shopify's CDN serves WebP automatically when the browser accepts it.

**Static sites and frameworks.** Next.js `<Image>`, Astro `<Image>` and Hugo's image processing resize and convert at build; the remaining job is the `sizes` attribute, which none of them can guess.

**Image CDNs.** Cloudinary, imgix, Cloudflare Images and Bunny Optimizer resize and convert on request from one original; a URL parameter picks the width. Good for sites with many images and no build step.

## Verify

- The table shows no file above 300 KB, no "not set" in the displayed column, and no 404.
- The heavy, oversized and missing-dimensions findings pass.
- The speed module's image findings (`image-formats`, `image-sizing`) show near-zero savings.
- LCP in the speed module drops; if the hero is the LCP element, it should now load within 2.5 s on mobile.

## Common mistakes

- **Uploading originals.** A 4000×3000 camera file scaled with CSS. Export at display size.
- **Lazy-loading the hero.** Adds hundreds of milliseconds to LCP for no gain.
- **`height: auto` without attributes.** CSS alone does not reserve space; the ratio has to come from the attributes or `aspect-ratio`.
- **Converting screenshots to JPEG.** Text becomes fuzzy; use PNG or lossless WebP.
- **Three optimisation plugins.** Each rewrites the same tags; pick one.
- **Serving the 2× file to every screen.** `srcset` exists so that a 375-pixel phone gets the 750-pixel file, not the 1600-pixel one.
