ImagePDF.Tools
Technical

How Browsers Actually Handle Your Images

N
NikolaLast updated on June 20, 2026 · 9 min read

Summary

File size is half the story. After bytes arrive, browsers decode and composite images in ways that affect memory and rendering. Here's what actually happens.

You compress a hero image from 800 KB to 120 KB, check the Lighthouse score, and the LCP is still 3 seconds. The file is smaller, why is it still slow? Because file size and rendering cost are two different things. The browser's work begins when the download ends, and that work, decoding, rasterising, compositing, has its own cost in memory, battery, and time.

Most image optimisation advice stops at the wire: smaller file, faster page. Performance audits reinforce this because they measure bytes transferred. But a developer who understands what the browser does after receiving those bytes can find the optimisations that file size alone cannot fix: GPU memory pressure from oversized images, decode jank from large synchronous decodes, and layer explosion from over-eager will-change usage.

Responsive images pipeline: browser network request, decode, rasterise, composite to screen | ImagePDF.Tools
The journey from compressed bytes to pixels on screen involves four distinct browser operations.

Step 1: The Network, Bytes Arrive

The browser's network stack fetches the image based on its priority. Priority determines when it starts downloading, which directly affects Largest Contentful Paint (LCP). Three attributes control image loading priority:

  • loading="lazy", defers the network request until the image is near the viewport. Use for all below-fold images.
  • loading="eager" (default), starts the network request immediately. Correct for above-fold images.
  • fetchpriority="high", signals that this is the LCP image. The browser boosts its priority in the download queue. Use on exactly one image, the hero.
html
<!-- LCP image: eager load, high priority, sync decode for guaranteed paint -->
<img src="hero.jpg" loading="eager" fetchpriority="high"
     decoding="sync" width="1200" height="630" alt="Hero">

<!-- Below-fold images: lazy load, async decode -->
<img src="gallery-1.jpg" loading="lazy" decoding="async"
     width="800" height="600" alt="Gallery image">

Step 2: Decoding, Compressed Bytes 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. This is a CPU-intensive operation. A 500 KB JPEG of a 2000×1500px photo decodes to 2000 × 1500 × 4 bytes = 12 MB of raw bitmap data held in memory.

ℹ️

This is why a gallery page with ten 3 MB JPEGs can consume 300+ MB of memory even if the total network payload was 30 MB. The compressed file size and the decoded memory footprint are completely different numbers. On mobile devices with 2–4 GB RAM, this matters significantly.

Decoding is synchronous by default, it blocks the main thread, delaying rendering. The decoding="async" attribute tells the browser to decode off the main thread, preventing jank on large images at the cost of a brief delay before the image appears.

AttributeDecodes onWhen to useTrade-off
decoding="sync" (default)Main threadLCP / above-fold hero imagesBlocks rendering briefly, but guarantees timely paint
decoding="async"Off main threadBelow-fold, gallery images, lazy-loaded contentNo jank, but image appears slightly after scroll
decoding="auto"Browser decidesWhen you're not sureInconsistent behavior across browsers
Decoding strategies: when to use sync vs. async

Step 3: Rasterisation, Pixels Painted to GPU Texture

After decoding, the image is rasterised onto a GPU texture. This is where a critical optimisation opportunity lives: the CSS display size of an image has no effect on the GPU texture size. A 4000×3000px image displayed in a 400×300px CSS box still occupies a 4000×3000px GPU texture, wasting roughly 90% of that memory.

This is the argument for dimension right-sizing: a 400×300px image displayed at 400×300px (with a 2× retina version at 800×600px served via srcset) uses exactly the GPU memory it needs. The same image uploaded at 4000×3000px wastes 25× the GPU memory, for zero visual benefit at its displayed size.

Step 4: Compositing, Layer Assembly

The browser compositor combines image layers, applies CSS transforms, handles opacity, and sends the final frame to the screen. Most images live in the default layer and are composited together cheaply. The problem arises when CSS properties promote elements to their own compositor layer:

css
/* Layer promotion, use sparingly */
.animated-image {
  /* will-change promotes to own GPU layer */
  /* Use ONLY on elements that actually animate */
  will-change: transform;
}

/* Static images: no will-change */
/* They share the default layer and cost almost nothing to composite */
.product-photo {
  /* nothing, let the browser decide */
}

/* Danger: applying will-change to many elements = many GPU layers = OOM on mobile */
.grid-item {
  will-change: transform; /* BAD if applied to 50 grid items */
}
⚠️

Applying will-change to images in a large grid (product thumbnails, photo galleries) creates a separate GPU texture for every item. On a mobile device with limited GPU memory, this causes out-of-memory crashes or severely degraded performance. Only apply will-change to the specific element being animated, not to a container or grid item class.

Srcset: Letting the Browser Choose the Right Size

The srcset attribute solves the dimension mismatch problem at scale. You provide multiple image sizes; the browser picks the one that most closely matches the actual display size multiplied by the device pixel ratio. A 1x display at 800px wide downloads the 800px image. A 3x retina display at 400px wide downloads the 1200px image.

html
<!-- Provide 3 sizes; browser picks the appropriate one -->
<img
  src="photo-800.jpg"
  srcset="photo-400.jpg 400w,
          photo-800.jpg 800w,
          photo-1200.jpg 1200w"
  sizes="(max-width: 600px) 100vw,
         (max-width: 1024px) 50vw,
         800px"
  loading="lazy"
  decoding="async"
  alt="Product photo"
  width="800" height="600"
>

Memory Budget: What Your Images Actually Cost

Image dimensionsDecoded memoryDisplayed atMemory waste
4000×3000px (12MP)45.8 MB400×300px (CSS)~90% wasted
2000×1500px (3MP)11.4 MB800×600px (retina)~75% wasted
800×600px1.8 MB800×600px (1x) ✓None, correctly sized
400×300px + 800×600px srcset0.4–1.8 MBMatches viewport ✓None, browser picks right size
Decoded image memory footprint at various dimensions

The Practical Optimisation Checklist

  • Right-size source images: Use resize to match display dimensions before compressing. An oversized image at quality 80 uses far more GPU memory than a correctly-sized image at quality 80.
  • Use srcset for all non-decorative images: Provide at least 2× and 1× versions. The browser handles the rest.
  • Set width and height on every img: Prevents Cumulative Layout Shift (CLS) and lets the browser reserve layout space before the image loads.
  • fetchpriority="high" on the LCP image only: One image per page. Boosting multiple images defeats the purpose.
  • decoding="async" on below-fold images: Prevents decode operations from blocking main-thread work as users scroll.
  • will-change only on animated images: Never apply it to static images or to all items in a large grid.

Bottom Line

File size and rendering cost are different problems with different solutions. File size is solved by compression and format selection. Rendering cost is solved by dimension right-sizing, srcset, and careful use of loading attributes. The full optimisation stack requires both: a correctly-compressed image at the wrong dimensions still wastes GPU memory, and a correctly-sized image that loads at the wrong time still degrades LCP. Address both together and the performance wins compound.

Frequently asked questions

Why does my page use so much memory even after I compressed the images?
Compression reduces the download size, but the browser must decode images into raw pixel data before displaying them. A 500 KB JPEG can decompress to 12 MB of raw bitmap data in GPU memory. If your images are larger in pixels than their display size, most of that memory is wasted. Resize images to match their display dimensions to reduce both download size and GPU memory usage.
What does fetchpriority="high" do on an img tag?
It signals to the browser that this image is the Largest Contentful Paint (LCP) element and should be downloaded first. The browser raises its position in the network download queue, typically reducing LCP by 200–400ms on resource-constrained connections. Only use it on one image per page, the above-fold hero. Using it on multiple images dilutes the effect.
When should I use decoding="async" vs decoding="sync"?
Use decoding="sync" (or omit the attribute) on your LCP image, the main hero above the fold. Sync decoding blocks the main thread briefly but guarantees the image paints at the expected time. Use decoding="async" on all below-fold images. Async decoding moves the decode operation off the main thread, preventing jank as users scroll, at the cost of the image appearing slightly after it would with sync decoding.
Does lazy loading affect Core Web Vitals?
Yes, if misapplied. Never lazy-load above-fold images, it delays their network request, directly hurting LCP. Lazy loading is only beneficial for images that are below the fold (not visible without scrolling). A common mistake is applying loading="lazy" to all images including the hero. Reserve it for gallery images, product grids, and other content that requires scrolling to reach.
What is will-change and when should I use it on images?
will-change: transform promotes an element to its own GPU compositor layer. This is beneficial for images that actually animate (a hero that fades, zooms, or slides), preventing repaint on every animation frame. It is harmful for static images because each promoted layer consumes separate GPU texture memory. Never apply it to all items in a grid, use it only on the specific element being animated.

Sources & references

This article was researched and written by Nikola, drawing on the following primary sources and documentation:

Ready to try it?

All tools run entirely in your browser, no uploads, no account required.

Resize Image
You're offline, cached tools still work