How to Use decoding="async" and the img.decode() API
This guide covers the two explicit decode controls in Image Decoding & Placeholders, within Image & Media Optimization. Browsers have to decode an image before painting it, and they normally choose when and where to do it. Two tools let you influence that choice. The decoding attribute (sync, async or auto) tells the browser whether it may render other content before this image is decoded. The decode() method on an image element returns a promise that resolves when the image is decoded and ready to paint without delay.
They solve opposite problems. decoding="async" prevents a large image's decode from holding up the rest of a frame — useful for images that appear during scrolling or alongside other content. decode() lets you wait for decoding before putting an image on screen — useful when swapping images in a gallery or carousel, where inserting an undecoded image causes a blank flash or a dropped frame. Neither is a general speed-up, and applying them carelessly to the LCP image can make LCP worse.
Rapid Diagnosis
- Look for Image Decode events in a throttled trace during scroll or interactions; long ones on the main thread or within the interaction's frame indicate decode-related jank.
- Watch for flashes. In carousels or galleries, a blank or partially drawn frame when switching images indicates insertion before decoding.
- Check LCP render delay. If it grew after adding
decoding="async"site-wide, the hero is affected. - Check component defaults. Some image components set
decoding="async"on every image, including the hero.
Root Cause Analysis
1. Synchronous decode in the rendering path. Large images inserted and painted in the same frame can force a synchronous decode, delaying the frame.
2. Swaps without pre-decoding. Changing src on a visible image shows nothing (or the old image) until the new one decodes.
3. Async decode on the LCP image. Allowing the browser to paint other content first can push the hero's paint (and LCP) a frame later.
4. Too many simultaneous decodes. Galleries that decode dozens of large images at once compete for decoder threads and memory.
Step-by-Step Resolution
1. Leave the LCP image on default decoding
<img src="/img/hero-1200.avif" width="1200" height="675" fetchpriority="high" alt="…">
<!-- No decoding attribute: the browser treats it as important.
trade-off: if the hero is enormous and shares the frame with critical text,
decoding="async" might let text paint first — but LCP will usually be the
image anyway, so measure before changing it. -->
2. Use decoding="async" for non-LCP images
<img src="/img/card-400.avif" width="400" height="300" loading="lazy" decoding="async" alt="…">
<!-- trade-off: async images may appear a frame after surrounding content,
which is invisible for below-the-fold images and fine with a placeholder. -->
Expected outcome: scrolling through image-heavy content stays smooth because decodes do not hold up frames.
3. Pre-decode before swapping images
async function showSlide(imgEl, nextSrc) {
const next = new Image();
next.src = nextSrc;
try { await next.decode(); } catch { /* decode failed: fall back to normal swap */ }
imgEl.src = nextSrc; // now paints without a blank frame
}
// trade-off: the old slide stays visible until the next one is decoded, so
// rapid clicking feels slightly less immediate. Show a subtle loading indicator
// on the control for slow networks.
4. Pre-decode before revealing content on interaction
When a click reveals an image (an accordion, a modal, a tab), start loading and decoding on hover or focus, and await decode() before revealing, so the interaction's frame does not include decoding work.
Verification
Record a trace of scrolling and of the gallery or reveal interaction. Image Decode events should not sit inside interaction frames, and there should be no blank frames during swaps (check screenshots in the trace). LCP render delay should be unchanged or improved for the hero. For interactions that reveal images, INP's presentation delay should not include decode time.
Worked Example: A Product Gallery
A product page gallery swapped the main image by setting src on click. With 2000px AVIF images, each swap showed a blank frame for 60–90ms on mid-tier phones, and the click interaction's presentation delay included the decode, pushing INP to 260ms for gallery clicks. Switching to decode()-before-swap, preloading the next image on thumbnail hover, and serving gallery images at display size (1000px) removed the blank frames and brought gallery INP to 90ms.
Interaction With Lazy Loading and Placeholders
decoding="async" pairs naturally with loading="lazy" and sized placeholders: the browser requests the image as it nears the viewport, decodes it without holding frames, and paints it into an already-reserved box with a background colour or blurred preview. The placeholder makes any one-frame delay from async decoding invisible. For images revealed by script, decode() replaces the need for a placeholder by keeping the previous state visible until the new image is ready. Use the two techniques in their own contexts and they rarely conflict.
Common Mistakes
- Setting
decoding="async"on everything, including the hero. Can delay LCP slightly; measure. - Awaiting
decode()on the critical path at load. Delays rendering for no reason; use it for swaps. - Ignoring
decode()rejections. Decoding can fail (corrupt image, unsupported format); catch and fall back. - Calling
decode()on huge images repeatedly. Each call may trigger a decode; cache the decoded element or reuse it.
Edge Cases
Canvas drawing. drawImage with an undecoded image forces a synchronous decode on the main thread; await decode() (or use createImageBitmap) first.
createImageBitmap. Decodes off the main thread and returns a bitmap usable in canvas and workers — ideal for image processing and WebGL textures.
Browser differences. Browsers interpret decoding as a hint and differ in defaults; treat it as an optimisation, not a guarantee.
Memory. Pre-decoding many images holds many bitmaps in memory; pre-decode only the next one or two items.
FAQ
Does decoding="async" lazy-load images?
No. It controls when decoding may happen relative to painting, not when the image is requested. Use loading="lazy" for deferring requests.
Is img.decode() supported everywhere?
Yes in all modern browsers. Wrap it in try/catch, since it rejects for images that fail to decode.
Does decode() help LCP?
Not for the initial load. It helps when images are inserted later by script, avoiding blank frames and keeping decode work out of interaction frames.
Should I ever use decoding="sync"?
Rarely. It can avoid a flash for small images that must appear in the same frame as surrounding content, at the cost of delaying that frame. For most cases, decode() is the better tool.
What about background images in CSS?
CSS backgrounds have no decoding attribute. Preload and pre-decode them via an Image object and decode() if they are revealed by script, or use <img> for important images.
How do I know decode is my problem rather than download?
In the trace, compare when the image's network request finished with when it painted. A large gap filled by Image Decode events (or a busy main thread) points at decoding; a late network finish points at loading.
Does decode() work for SVG images?
Yes, it resolves when the SVG is ready to render; SVG decoding is usually cheap unless the file is complex, in which case rasterisation cost dominates rather than decoding.
Related
- Avoiding main-thread image decode jank — broader decode-jank patterns.
- Lazy-loading images in carousels — combining lazy loading with pre-decoding.
- Reducing presentation delay from large DOM updates — when reveals are slow for other reasons.