How Browsers Actually Handle Your Images
File size is only half the story. After the bytes arrive, browsers decode, rasterise, and composite images in ways that affect memory, battery, and rendering performance.
Most image optimisation advice treats delivery as a download problem: smaller file, faster page. Performance audits reinforce this because they measure bytes on the wire. But once the bytes arrive, the browser still has significant work to do — and that work can cause jank, memory pressure, and slow paints even when the download is fast.
Step 1: Decoding — From Compressed to Raw Pixels
The browser receives compressed image bytes (JPEG, WebP, etc.) and must decode them into raw RGBA pixel data before it can draw anything. A 500 KB JPEG of a 2000×1500px photo decodes to 2000 × 1500 × 4 bytes = ~11.4 MB of raw bitmap data in memory.
This is why a page with ten 2MB JPEGs can consume 200+ MB of GPU memory even if the total download was only 20 MB. The compressed file size and the decoded memory footprint are completely different numbers.
Step 2: Rasterisation and the Compositor
After decoding, the image is rasterised (painted) onto a GPU texture. The browser compositor combines image layers, applies CSS transforms, and sends the result to the screen. Large images that are scaled down in CSS still occupy full-resolution texture memory on the GPU — a 4000×3000px image displayed at 400×300px wastes 90% of that memory.
Step 3: Will-Change and Layer Promotion
CSS properties like will-change: transform promote an element to its own compositor layer. For large hero images that animate, this prevents repaint on every animation frame. The trade-off: each promoted layer occupies separate GPU texture memory.
/* Promote image to own layer for smooth animation */
.hero-image {
will-change: transform; /* uploads to GPU texture once */
}
/* Without will-change, CSS transforms trigger repaint on every frame */
/* Avoid on static images — unnecessary memory usage */Decoding: Synchronous vs. Async
By default, image decoding happens synchronously on the main thread, blocking rendering. The decoding="async" attribute tells the browser to decode off the main thread, preventing decode jank on large images.
<!-- async decode: won't block rendering, image appears when ready -->
<img src="large-photo.jpg" decoding="async" loading="lazy" alt="...">
<!-- sync decode (default): blocks main thread until decoded -->
<!-- Use for LCP images where you want guaranteed paint timing -->
<img src="hero.jpg" decoding="sync" fetchpriority="high" alt="...">What This Means for Optimisation
- ●Right-size source images: A 4000×3000px image displayed at 400×300px wastes 100× the GPU memory. Resize to display dimensions.
- ●Use srcset: Serve different resolutions for different viewports — the browser picks the best match automatically.
- ●Add decoding="async": Prevents decode jank for below-fold images, at the cost of a brief flash when scrolling.
- ●Avoid will-change on static images: Layer promotion consumes GPU memory on every device viewing the page.
- ●Compress aggressively for thumbnails: A 50×50px thumbnail decoded to raw pixels is trivial — quality 60 is fine.
The most overlooked optimisation is dimension right-sizing. Use our image resizer to set exact pixel dimensions before compression — a correctly-sized image at quality 80 will use far less GPU memory than an oversized image at quality 60.
Ready to try it?
All tools run entirely in your browser — no uploads, no account required.
Resize Image