Skip to main content

Decoding async: the tiny HTML attribute that trims your LCP

A quick, honest guide to the HTML decoding attribute — what it does, when async helps, when it hurts, and how it fits into Core Web Vitals.

6 min readBy
  • performance
  • core-web-vitals
  • lcp
  • html
  • images

Adding decoding="async" to your non-LCP <img> tags tells the browser it's allowed to decode the image off the critical rendering path — so the headline and hero paint first, and everything else slots in when it's ready. It's one attribute. Four letters. On image-heavy pages it can shave real milliseconds off your LCP at the 75th percentile. And almost nobody sets it.

This post is the 10-minute version of a conversation I have with clients about once a month.

What LCP actually is (the 45-second version)

LCP stands for Largest Contentful Paint. It measures how long it takes for the biggest visible element on your page — usually the hero image or the headline — to finish painting. Google's "good" threshold is under 2.5 seconds at the 75th percentile of real users.

That "75th percentile" phrase is doing a lot of work. It means 75% of your real users have to see "good" LCP — not you on your MacBook with fibre. That tail 25% is running on mid-range Androids over 4G on a train, and that's where most sites quietly fail the metric.

Where image decoding fits into all that

An <img> tag goes through three stages before you actually see pixels:

  1. Fetch — download the image bytes from the network.
  2. Decode — turn those JPEG / PNG / AVIF bytes into raw pixel data in RAM.
  3. Paint — put the pixels on the screen.

Most people only optimize step 1. Smaller files, AVIF, CDNs, preload — all step-one tricks.

Step 2 is where decoding lives. Decoding a 1200×800 JPEG on a mid-range phone isn't free — it's a real chunk of main-thread work. Multiply that by the eight-to-twelve images on a typical marketing page and you've got a genuine slice of the frame budget being spent on pixel-unpacking.

Here's the part that matters: while the browser is busy decoding the footer logo and the five client badges and that decorative SVG behind the headline, it isn't painting your LCP element.

That's the slot decoding="async" was invented for.

The three knobs everyone confuses

Image performance in 2025 has three HTML attributes, and I watch engineers conflate them every week:

  • loading — controls when to fetch. Values: eager, lazy.
  • fetchpriority — controls how important that fetch is. Values: high, low, auto. (Chrome for Developers writeup.)
  • decoding — controls how to decode after the bytes arrive. Values: sync, async, auto. (MDN reference.)

They're completely orthogonal. loading="lazy" decoding="async" is the default posture for below-the-fold content. loading="eager" fetchpriority="high" is what you slap on your LCP image. decoding is the knob that moves LCP for the rest of the page.

If you only read one paragraph of this post, it's this one.

When decoding="async" is a strict win

On literally every non-LCP image. Think:

  • Decorative SVGs (the lines, dots, and waveforms behind headings).
  • Footer logos and client badges.
  • Tech-stack badges on service cards.
  • Blog-post content images below the fold.

None of these are the LCP candidate. None of them need to block the first big paint. Async decoding tells the browser: "I'm not precious. Paint the text first and get to me when you can."

I set it on every raw <img> we ship. It's free literally free.

When decoding="async" is actively wrong

On the LCP image itself.

Counter-intuitive, right? If async is faster, shouldn't async be faster for everything?

No. decoding="async" is permission for the browser to delay painting that image. On the LCP image that's the opposite of what you want — you want it decoded and painted as soon as physically possible, because that's the image the metric is measuring.

For the LCP image the right answer is:

  • Use fetchpriority="high". This is the real LCP mover. It tells the browser to dispatch this fetch ahead of everything else, including other images on the page.
  • Leave decoding unset (which means auto), or set decoding="sync" only if profiling shows it actually helps. Modern Chrome and Safari have pretty good "is this the LCP candidate?" heuristics and they usually get it right on their own.

Translation: fetchpriority is the striker. decoding is the midfielder. Don't ask the midfielder to score.

The Next.js cheat sheet ⚡

If you're on Next.js, most of this is already handled for you — but only for one of the two tag types you'll ship:

  • next/image sets decoding="async" on every rendered <img> automatically. Non-LCP case: done for free.
  • priority prop on next/image emits fetchpriority="high" plus loading="eager" and lets the browser auto-pick decoding. LCP case: also done for free.
  • Raw <img> tags — the kind you actually need for SVG backgrounds, dot patterns, decorative lines, OG-preview placeholders — get nothing by default. You have to set decoding="async" yourself.

Audit your raw <img> tags. On a typical Next.js marketing site I've seen this miss on six-to-ten decorative SVGs per page. That's the bucket where async decoding has the biggest compounded effect, because each one is a tiny main-thread interruption that was competing with the LCP paint for no good reason.

What actually moves LCP — ranked

I don't want anyone reading this and walking away thinking async decoding is the main character. It isn't. Here's the honest ranked list, biggest-mover first:

  1. Smaller, better-format LCP image. AVIF over WebP over JPEG. Usually 30–50% size reduction with no visible quality loss.
  2. fetchpriority="high" on the LCP image. Often a free 100–200ms.
  3. Preload the LCP asset with <link rel="preload" as="image">, or the priority prop if you're on next/image.
  4. Faster server / edge cache. If your TTFB is 800ms, nothing downstream saves you.
  5. decoding="async" on every non-LCP <img>. The quiet one that compounds across the page.

Async decoding is the unglamorous #5 that stacks with the other four. It's not going to take a 4s LCP to 2s by itself. But combined with the other items on the list, it's the difference between "barely passing" and "comfortably passing" on a phone that isn't yours.

TL;DR 🎯

  • Set decoding="async" on every <img> that is not the LCP image. Do nothing to the LCP one.
  • next/image handles the common case automatically. Raw <img> tags do not.
  • Don't conflate loading, fetchpriority, and decoding — three different knobs, three different jobs.
  • fetchpriority="high" is the real LCP mover. Async decoding is the supporting act.

Want someone to audit the boring stuff?

Most Core Web Vitals problems I see on real client sites aren't dramatic. They're a missing fetchpriority on a hero image, twelve decorative SVGs without decoding="async", and a web font that swaps at the wrong moment. Add them up and you're 300ms slower than you need to be, for free, on every page load.

If that's the kind of thing you'd rather not spend a Saturday on, send us the URL. We'll run it through the same audit we run on our own site and tell you honestly what's worth fixing first — and what isn't.

Stop reading.
Start building.

If this post nudged something loose, let's talk. Tell us what you're trying to ship and we'll tell you how we'd approach it.