ScreenshotNeo

BlogEngineering

Progressive Image Decoding: Improve Image Loading Performance

Learn when to use decoding="async", how to wait for an image with decode(), and how loading, priority, sizing, and compression affect image performance.

By the ScreenshotNeo team4 October 20268 min read

To improve image presentation with progressive decoding, use decoding="async" selectively when surrounding content should be able to paint before an image is ready. For images that JavaScript inserts or swaps, use HTMLImageElement.decode() and reveal the image after its promise resolves. Neither technique makes the file download faster; decoding is a browser scheduling hint, while download size, request priority, and lazy loading are separate concerns.

There is no universal performance gain from adding decoding="async" to every image. Measure the page and keep the browser’s auto choice when you have no specific reason to change scheduling.

What progressive image decoding changes

A browser must fetch image bytes and decode them into pixels before it can present the image. The HTML decoding attribute hints how the browser should coordinate that decoding with rendering:

Value or method What it requests When it can help
auto Let the browser choose the scheduling approach. This is the default. Use when you have no measured or specific need to steer presentation.
async Allow other content to render before this image is decoded and presented. Use selectively when the surrounding content should paint without waiting for the image.
sync Ask the browser to present the image and other content in a coordinated rendering update. Consider when coordinated presentation matters, then verify the behavior in the target browsers.
img.decode() Return a promise that resolves when a specific image has decoded. Use in JavaScript to wait before inserting or swapping an image into view.

These are scheduling controls, not guarantees. On static <img> markup, the visible effect can be subtle. Scheduling can be more noticeable when JavaScript inserts an image or replaces one that is already visible. MDN describes async as allowing the next paint to proceed without waiting for the image to decode. MDN: HTMLImageElement.decoding

Use decoding=”async” selectively in HTML

Add the attribute to images where letting other content paint first is desirable. You can use it alongside dimensions, responsive sources, and loading hints because those address different parts of image delivery and presentation.

<img
  src="/images/article-960.webp"
  alt="A mountain trail at sunrise"
  width="960"
  height="640"
  decoding="async"
>

For a responsive image, preserve the same scheduling hint while allowing the browser to choose an appropriate source:

<img
  src="/images/article-960.webp"
  srcset="/images/article-480.webp 480w, /images/article-960.webp 960w, /images/article-1440.webp 1440w"
  sizes="(max-width: 600px) 100vw, 960px"
  width="960"
  height="640"
  alt="A mountain trail at sunrise"
  decoding="async"
>

Do not apply async to every image just because it sounds faster. The hint permits asynchronous presentation; it does not promise a faster request, faster decode, or a Core Web Vitals improvement. Leave auto in place unless you have a reason to guide scheduling and can assess the result on your pages.

Wait for a dynamic image with decode()

When JavaScript creates an image or swaps a source, waiting for decode() before revealing it helps avoid showing an empty image while its pixels are not ready. Keep the existing content or a fallback visible if loading or decoding fails.

async function revealImage(url, container) {
  const img = new Image();
  img.alt = "A mountain trail at sunrise";
  img.width = 960;
  img.height = 640;
  img.decoding = "async";
  img.src = url;

  try {
    await img.decode();
    container.replaceChildren(img);
  } catch (error) {
    // Preserve the current content or show a fallback image.
    console.error("Image could not be loaded or decoded", error);
  }
}

const container = document.querySelector("#preview");
if (container) {
  revealImage("/images/article-960.webp", container);
}

decode() resolves when the image is decoded and ready to use. Its promise can reject if the image data is invalid or the image could not be loaded. Handle that rejection rather than removing the old image first. See MDN: HTMLImageElement.decode().

Swap an existing image without a blank transition

For a source change, decode a separate image first and replace the visible image only after decoding succeeds:

async function swapImage(currentImg, nextUrl) {
  const nextImg = new Image();
  nextImg.alt = currentImg.alt;
  nextImg.width = currentImg.width;
  nextImg.height = currentImg.height;
  nextImg.decoding = "async";
  nextImg.src = nextUrl;

  try {
    await nextImg.decode();
    currentImg.replaceWith(nextImg);
    return nextImg;
  } catch (error) {
    // Keep currentImg on screen if the new source fails.
    console.error("Keeping the current image", error);
    return currentImg;
  }
}

If a user can trigger several source changes quickly, an earlier request may finish after a newer one. Track a request identifier or cancel obsolete fetches in your application so an old image does not overwrite the latest choice.

Keep decoding separate from fetching and layout

Image performance has several stages. Choose the control that addresses the stage you need:

Need Use What it does not do
Let an offscreen image wait until it is near the viewport loading="lazy" It does not control decode scheduling. Avoid lazy-loading an image likely to be the initial viewport or LCP image as a blanket rule.
Give an important image greater relative request importance fetchpriority="high" where appropriate It does not make decoding asynchronous or reduce file size.
Let other content paint before an image is presented decoding="async" It does not prioritize, defer, or accelerate the network request.
Prevent layout shifts while an image loads Set width and height, or reserve equivalent space in CSS It does not shorten download or decode time.
Reduce transfer time and decoding work Serve appropriately sized, compressed images and use responsive sources It does not dictate how the browser schedules presentation.

For example, an initially visible hero image may deserve prompt fetching and reserved dimensions, while an offscreen gallery image may be lazy-loaded. Decoding hints can be considered separately for each image. Browser scheduling and the actual page workload determine whether a hint has an observable effect. Related guidance: web.dev: browser-level image lazy loading and web.dev: optimize CLS.

A practical decision process

  1. Identify the bottleneck. Determine whether the problem is a late request, a large transfer, decode/render contention, or layout movement. The decoding attribute only addresses presentation scheduling.
  2. Fix the source first. Serve an image close to its rendered dimensions, use responsive sources where needed, and compress it appropriately.
  3. Reserve layout space. Set intrinsic dimensions or an equivalent aspect ratio so content does not jump when the image arrives.
  4. Choose fetch behavior. Lazy-load genuinely offscreen images. Do not delay an image needed immediately just to apply a blanket rule.
  5. Try asynchronous presentation where it fits. Add decoding="async" when other content should be allowed to paint first.
  6. Use a readiness promise for dynamic UI. Await decode() before revealing an inserted or replacement image, with a fallback for rejection.
  7. Measure representative pages. Compare the actual user-visible result on the browsers and devices that matter to your page. Do not assume a universal gain.

Troubleshooting

Symptom Likely cause What to do
Adding decoding="async" changes nothing visibly The browser already schedules the static image suitably, or decoding is not the bottleneck. Keep auto if there is no measurable reason to steer scheduling. Check transfer size, request timing, and layout separately.
The image appears late despite async async does not speed up fetching or decoding and may allow other content to paint first. Inspect network timing and asset dimensions. Serve a suitable file and set fetch priority only when the image warrants it.
The page jumps when an image appears The browser did not have space reserved for the image. Set width and height or reserve the aspect ratio in CSS, including for lazy images.
decode() rejects The URL failed to load, the response was not usable image data, or decoding failed. Catch the rejection, retain existing content, and verify the URL and response. Show a fallback when appropriate.
An older image replaces a newer selection Concurrent updates completed out of order. Use an update token and ignore results from stale requests, or abort obsolete fetches when your loading design permits it.
Lazy images are missing from an initial screenshot or first view The image is offscreen and its lazy request has not started yet. Do not lazy-load content required immediately. Trigger loading when appropriate and wait for the relevant image before capture.

Performance, reliability, and cost

The main cost of image performance work is usually in bytes transferred, image processing, and time spent diagnosing the actual bottleneck. Decode scheduling alone does not reduce network bytes or guarantee a faster render. Responsive sources and compression can reduce transfer cost; correct dimensions also improve layout stability. Avoid adding JavaScript solely to wait for static images when declarative markup is enough.

For dynamic updates, keep a previous image or explicit fallback until the new image has loaded and decoded. A promise rejection is a normal failure path to handle, not a reason to leave the UI empty. If the update can be superseded, prevent stale completions from winning.

Or skip the browser setup

If the goal is to obtain a clean screenshot of a page rather than implement image behavior inside your own site, ScreenshotNeo is a website screenshot API and MCP server for developers. Its one-call API returns an image or PDF; the browser-side decoding guidance above still applies when you build a webpage that displays those images.

curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://stripe.com -o shot.webp

See the ScreenshotNeo API documentation for the request options. Cookie banners are accepted like a visitor and 60+ known consent platforms, newsletter popups, and chat widgets are removed before the shot. Bot checks, blank pages, timeouts, failed loads, and cache hits cost nothing, and response headers identify the page verdict and billing status. Its MCP server lets AI agents use take_screenshot, get_page_info, and capture_pdf. The free plan includes 1,000 screenshots a month with no card; paid plans start at $5 for 3,000.

Create a free ScreenshotNeo account and get 1,000 screenshots a month with no card.

FAQ

Should I use decoding="async" on every image?

No. It is a scheduling hint, and the effect depends on the page and browser. Use it selectively and measure; otherwise, leave the browser’s auto choice.

Does asynchronous decoding make an image download faster?

No. It affects coordination of decoding and presentation. Request timing and file transfer are separate.

When should I use decode() instead of the attribute?

Use decode() when script needs to know that a particular image is decoded before inserting or swapping it. Use the attribute to hint scheduling for an image element.

Does decode() guarantee the image will be visible?

It indicates the image has decoded and is ready for use. Your script still has to insert or reveal it, and should handle promise rejection.