# Why Don’t Websites Put All Their Images Into One Giant JPEG? (Nerd-Sniped by My Brain)

I had a simple question:

> Why do websites load lots of individual images instead of stitching them into one giant image and cropping out the pieces they need?

At first glance, an image atlas sounds great.

Instead of this:

```txt
image-1.jpg
image-2.jpg
image-3.jpg
...
image-100.jpg
```

You create this:

```txt
site-atlas.jpg
```

Then each UI tile crops a specific region from the atlas.

That would mean:

*   fewer network requests
    
*   images arrive together
    
*   no staggered popping
    
*   maybe better perceived loading
    
*   maybe less request overhead
    

Not a new idea by any means. Games and UI libraries have used sprite sheets and texture atlases forever. The question is: **why isn’t this the default for websites?**

## The experiment

I compared three approaches:

1.  **Individual optimized images**
    
    *   14 separate optimized JPG files
        
    *   rendered as normal `<img>` elements
        
2.  **Canvas atlas**
    
    *   one stitched atlas JPG
        
    *   each tile rendered by cropping from the atlas into `<canvas>`
        
3.  **CSS background atlas**
    
    *   one stitched atlas JPG
        
    *   each tile rendered with `background-image`, `background-size`, and `background-position`
        

The atlas was regenerated from the same optimized images, so the comparison was more fair.

**NOTE: I ran the experiment by hosting it locally. so all the number you see are when you have the application served using a python server running locally.**

> If you want to poke at it yourself, the experiment is live here: [https://daviddodda.com/experiments/img-atlas/](https://daviddodda.com/experiments/img-atlas/)  
> note: make sure you disable cache. try each version a couple of times.

* * *

## What I measured

I focused on three headline metrics.

### 1\. Transferred

How many bytes were downloaded?

### 2\. Network complete

When did the last required image resource finish downloading?

### 3\. Visible / ready

When was the image grid actually ready to see?

This last one matters because network completion is not the full story. The browser still has to decode images, rasterize, paint, composite, and show pixels.

* * *

## Results

On a remote machine running Chromium, all files hosted locally, 10 runs each:

| Approach | Transferred | Network avg | Visible avg | Visible median |
| --- | --- | --- | --- | --- |
| Individual images | 1.6 MB | 34.4 ms | 292.4 ms | 289.1 ms |
| Canvas atlas | 2.3 MB | 12.6 ms | 454.2 ms | 453.7 ms |
| CSS background atlas | 2.3 MB | 12.9 ms | 222.5 ms | 217.5 ms |

The surprising result:

> The CSS background atlas was the fastest to visible.

* * *

## The atlas won the network

The atlas had a clear network advantage:

```txt
Individual images: ~34 ms network complete
Atlas:             ~13 ms network complete
```

Well, one larger request has less overhead than many smaller requests.

This effect is especially visible when the server/browser are using less optimal connection behavior. In my test, Chromium reported `http/1.0` for the local server, so request overhead was more obvious than it would be under HTTP/2 or HTTP/3.

With modern HTTP/2 and HTTP/3, many individual image requests are less painful because requests can be multiplexed over one connection. But request overhead still exists.

* * *

## The atlas has dummy pixels

The individual images transferred:

```txt
1.6 MB
```

The regenerated atlas transferred:

```txt
2.3 MB
```

Why?

Because an atlas is a rectangle. Real images have different aspect ratios. When you pack them into one big rectangular sheet, you often create empty space.

In my case:

```txt
Individual images total pixels: ~23.8M
Atlas pixels:                   ~31.2M
```

That is about 31% extra pixel area.

So even though the atlas used one request, it transferred more data and required the browser to decode a bigger image surface.

* * *

## Canvas atlas sucks

The canvas atlas looked like it should be fast (thought modern hardware was fast enough). It loaded one atlas image, then cropped each tile into a canvas.

But the results were poor:

```txt
Canvas atlas visible avg: ~454 ms
```

The breakdown showed:

```txt
Atlas network complete:        ~13 ms
Atlas decode wait after load:  ~167 ms
Canvas crop drawing:           ~6 ms
Paint/composite wait:          ~261 ms
```

The actual JavaScript canvas drawing was not expensive.

The expensive part was making all those canvas results visible.

That means the bottleneck was not:

```js
ctx.drawImage(...)
```

It was the browser’s later paint/composite work.

* * *

## CSS background atlas does not suck

The CSS background atlas used normal DOM elements:

```css
.tile {
  background-image: url("./atlas/animals.optimized.jpg");
  background-size: ...;
  background-position: ...;
}
```

This was much faster:

```txt
CSS atlas visible avg: ~222 ms
```

The breakdown:

```txt
Atlas network complete:        ~13 ms
Atlas decode wait after load:  ~166 ms
Apply CSS background crops:    ~2 ms
Paint/composite wait:          ~36 ms
```

The decode cost was still there. But paint/composite was dramatically better than the canvas version.

So if you are going to do image atlasing in normal web UI, CSS backgrounds may be much better than drawing many cropped canvases.

* * *

## When atlases do make sense

They are great for:

*   icons
    
*   sprites
    
*   emoji sheets
    
*   game textures
    
*   small repeated UI assets
    
*   known fixed-size tile sets
    
*   maps or tile-like interfaces
    
*   cases where all assets are needed immediately
    

They are less great for:

*   photo galleries
    
*   blog images
    
*   user-generated content
    
*   responsive images
    
*   content-heavy websites
    
*   long scrolling pages
    
*   frequently changing assets
    

now, don't go getting any ideas about rewriting your website's image pipeline to use image atlas. here are some reason why it's a really bad idea.

## Why not use an image atlas?

### 1\. You lose lazy loading

With individual images, the browser can load only what is needed:

```html
<img loading="lazy" src="photo-83.jpg" />
```

With a giant atlas, loading one image means loading everything in that atlas.

That is great if you need everything immediately.

It is terrible if the user only sees 5% of the images.

* * *

### 2\. You lose responsive images

The web has powerful responsive image tools:

```html
<img
  src="small.jpg"
  srcset="small.jpg 480w, medium.jpg 960w, large.jpg 1600w"
  sizes="(max-width: 600px) 100vw, 300px"
/>
```

The browser can choose the right image for the device, viewport, DPR, and network.

With a giant atlas, this becomes much harder. You may need multiple atlases:

```txt
atlas-1x.jpg
atlas-2x.jpg
atlas-mobile.jpg
atlas-desktop.jpg
atlas-avif.avif
atlas-webp.webp
```

The combinatorial complexity gets ugly quickly.

* * *

### 3\. You often transfer extra pixels

Atlases require packing. Packing creates waste.

If the images have different shapes, the atlas may contain a lot of empty or unused area.

Even a good packing algorithm cannot always avoid this.

In my test, the atlas had about 31% more pixel area than the individual images.

* * *

### 4\. One changed image invalidates the whole atlas

With individual images:

```txt
avatar-123.jpg changed
```

Only that image needs a new URL/cache entry.

With an atlas:

```txt
site-atlas.jpg changed
```

The whole atlas cache is invalidated.

That is bad for websites where content changes often.

* * *

### 5\. Prioritization gets worse

Browsers are good at prioritizing resources.

The hero image can be high priority. Below-the-fold images can be lazy. Tiny thumbnails can wait.

With a giant atlas, everything has one priority.

You cannot easily say:

```txt
Load the hero image first,
then visible thumbnails,
then below-the-fold images.
```

The atlas is all-or-nothing.

* * *

### 6\. Memory can be worse

A compressed JPG might be 2 MB on the network, but decoded pixels are much larger.

Decoded RGBA memory is roughly:

```txt
width × height × 4 bytes
```

A large atlas can become a huge decoded surface.

In my first broken atlas attempt, the atlas was:

```txt
9744 x 25942
```

That is around:

```txt
~1 GB decoded RGBA
```

Even if the file downloads quickly, that is a lot for the browser to decode, rasterize, and paint.

* * *

### 7\. Accessibility and semantics are worse

An `<img>` has natural semantics:

```html
<img src="rabbit.jpg" alt="Rabbit in grass" />
```

A CSS background image is decorative by default. If the image is meaningful content, you need to rebuild semantics with ARIA or hidden text.

That is doable, but it is extra work and easier to get wrong.

* * *

### 8\. Browser optimizations favor normal images

Browsers have spent decades optimizing:

```txt
<img>
<picture>
srcset
lazy loading
decoding
fetch priority
cache behavior
progressive rendering
```

If you use an atlas, you bypass some of that machinery and take on more responsibility yourself.

Sometimes that is worth it. Often it is not.

* * *

## What I learned

<mark class="bg-yellow-200 dark:bg-yellow-500/30">Every approach has its niche use case (shocker).</mark>

My brain nerd-sniped me into exploring and writing about this. It was fun seeing the cute animals load in though.
