Image Optimization for Core Web Vitals

On most pages the Largest Contentful Paint element is an image, which makes image work the highest-leverage performance change available. The order you do it in matters more than the tooling.

By ZyffPublished 9 min read

The short version

  • Serve fewer bytes first. A modern format plus correct dimensions usually cuts a hero image by more than half.
  • Never resize with CSS alone. Downloading a 3000px image to display it at 800px wastes the bytes that decide LCP.
  • Always set width and height, or an aspect-ratio, so the browser reserves space and nothing shifts.
  • Do not lazy-load the LCP image. It delays exactly the request you need earliest.
  • Add fetchpriority="high" to the hero image and lazy-load everything below the fold.

Core Web Vitals measure three things, and images dominate two of them. Largest Contentful Paint is the time until the biggest above-the-fold element finishes rendering, and on a typical page that element is a hero image or a product photo. Cumulative Layout Shift measures unexpected movement, and the most common cause is an image that loads without reserved space and pushes everything below it down.

That makes images the first place to look, and it also means the work is bounded — a handful of specific changes, most of which are one-time. The order below is by effect: serving fewer bytes matters most, then reserving space, then telling the browser what to prioritise. Doing them out of order tends to produce a lot of activity and a small improvement.

One framing worth keeping: the metric is what a real visitor experiences on a mid-range phone over mobile data, not what a lab test reports on a fast connection. A 900KB hero image that scores fine locally is a slow page for a large share of real traffic, and that is what the field data will say.

Serve fewer bytes: format and dimensions

The LCP image cannot render before it has downloaded, so its size in bytes sets a floor on the metric that no amount of loading-strategy tuning gets under. Two changes address it, and together they routinely take a hero image from 900KB to under 150KB.

The first is format. Converting a JPEG hero to WebP typically saves 25 to 35% at matching quality, and AVIF saves more again. Both are supported by every current browser, so this is close to free — the same picture, considerably fewer bytes, no visible difference.

The second is dimensions, and it is the one people skip. An image displayed 800 pixels wide should be somewhere around 1600 pixels of source data to look sharp on a high-density screen — not 4000. Shipping a camera-resolution file and letting CSS scale it down means the browser downloads and decodes every one of those pixels before discarding most of them, and the cost lands squarely on LCP.

Quality is the third, smaller lever. Around 80 is the point where further reductions start to be visible on photographs, and most images shipped from a design tool are well above that with nothing to show for it.

  • Convert to WebP as a baseline; AVIF for large hero images where bytes matter most.
  • Resize so the source is at most about twice the largest displayed width.
  • Quality around 80 for photographs; higher only for screenshots and flat graphics.
  • Strip embedded thumbnails and metadata, which can be a surprising share of a small file.

Reserve space so nothing shifts

Cumulative Layout Shift from images has one cause and one fix. If an image element has no dimensions, the browser does not know how tall it will be, lays the page out as if it were empty, and then reflows everything below once the image arrives. That reflow is the shift.

The fix is to set the width and height attributes on every image element, with the real pixel dimensions of the file. Modern browsers use them to compute an aspect ratio and reserve correctly proportioned space even when CSS overrides the rendered size — so setting them does not conflict with responsive styling, which is the old objection to doing it. Where the dimensions are genuinely unknown ahead of time, an explicit aspect-ratio in CSS achieves the same thing.

The pattern to watch for is any image whose dimensions come from data — a CMS field, an API response, a user upload. Those are the ones that ship without attributes, because there was no literal to type. Storing intrinsic width and height alongside the file at upload time is the durable fix, and it is much easier to add at that point than to backfill later.

Load the right image at the right time

Once the bytes are small and the space is reserved, the remaining wins are about ordering. Three attributes do the work, and two of them are frequently used backwards.

Lazy loading defers an image until it is near the viewport, which is exactly right for everything below the fold and exactly wrong for the LCP element. A lazily-loaded hero image cannot start downloading until layout and script have progressed far enough to determine it is visible, which adds a delay to the one request you most wanted early. Applying loading="lazy" to every image on a page is a common blanket change that makes LCP measurably worse.

Priority hints are the counterpart. Adding fetchpriority="high" to the hero image tells the browser to fetch it ahead of other subresources, which is useful because a browser cannot know which image is the LCP element until it has done enough work to lay the page out. This is one attribute on one element per page and it is often worth a couple of hundred milliseconds.

Responsive sources are the third. A srcset with a sizes attribute lets the browser pick a file matched to the device's viewport and pixel density, so a phone downloads a phone-sized image instead of the desktop one. That is where the largest real-world saving lives, because mobile is where the constraint bites and where a single fixed-size image is most wasteful.

ImageloadingfetchpriorityAlso
Hero / LCP elementeager (default)highwidth, height, srcset
Above the fold, not LCPeager (default)auto (default)width, height
Below the foldlazyauto (default)width, height
Decorative backgroundlazylowCSS aspect-ratio
Which attribute goes where

Preload only when the URL is discoverable late

A preload link tells the browser to start fetching a resource before it would otherwise find it. For images this is worth doing in one specific situation: when the hero image's URL is not in the initial HTML — because it comes from CSS, or from JavaScript, or from a client-rendered component — the browser cannot begin the request until it has parsed and executed enough to discover it, and preloading recovers that lost time.

When the image is a plain element in the server-rendered HTML, the browser's preload scanner finds it almost immediately and a preload link adds nothing. Preloading several images makes things actively worse: they compete with each other and with the resources the page needs to render at all.

So the rule is one preload at most, for a hero image whose URL is genuinely late-discovered, and otherwise put the image in the HTML and let fetchpriority do the work.

Measure with field data, not just a lab score

A lab test runs once, on one connection, from one location, and gives a repeatable score that is useful for comparing two versions of a page. It is not what Core Web Vitals assessment uses. That comes from field data — real visits on real devices — which is aggregated over 28 days and weighted toward the mid-range Android phones that make up most mobile traffic.

The practical consequence is patience. A fix deployed today does not move the field number today, because the window still contains four weeks of the old behaviour. Use lab tools to confirm the change did what you intended, and the field data to confirm it mattered.

It also means the LCP element may not be what you assume. Lab tools report which element they measured, and it is regularly a surprise — a background image, a logo, a block of text on a page where the image loaded faster than expected. Check what is actually being measured before optimising the wrong thing.

Try it on Image Compressor

Free, no signup, no watermark. Runs on Zyff’s servers, so you get the same result on a phone as on a desktop.

Open the tool

How to optimize images for Core Web Vitals

  1. Find the real LCP element

    Run the page through a lab tool and read which element it reports as the Largest Contentful Paint. Do not assume it is the hero image — it is frequently a background image or a different element entirely, and optimising the wrong one changes nothing.

  2. Resize it to what is actually displayed

    Check the largest width the image is ever rendered at, then use Zyff's resizer to bring the source to about twice that for high-density screens. A 4000px file displayed at 800px is downloading roughly 25 times more pixel data than it needs.

  3. Convert to a modern format

    Run it through the compressor as WebP at quality 80, or AVIF if it is a large hero image. This alone typically removes 25 to 50% of the bytes with no visible change, and the LCP image cannot render before its bytes have arrived.

  4. Set width and height on every image

    Add the real intrinsic dimensions as attributes so the browser reserves correctly proportioned space. This is the whole fix for image-driven layout shift, and it does not conflict with responsive CSS — the attributes only establish the aspect ratio.

  5. Fix the loading attributes

    Remove loading="lazy" from anything above the fold, add fetchpriority="high" to the LCP image, and add loading="lazy" to everything below the fold. A blanket lazy-load across every image is a common change that makes LCP worse.

  6. Add responsive sources for mobile

    Generate two or three widths of each significant image and offer them with srcset and sizes, so a phone downloads a phone-sized file. This is where the largest real-world saving is, because field data is weighted toward mid-range mobile devices.

  7. Re-measure, then wait for field data

    Confirm with a lab tool that LCP improved and layout shift dropped. Then give the field data time — it aggregates 28 days of real visits, so the reported number moves gradually rather than on deploy.

At a glance

Formats worth servingAVIF and WebP, with JPEG as a fallback
Typical WebP savingAbout 25-35% against JPEG at matching quality
Sensible qualityAbout 80 for photographs
Source widthRoughly twice the largest displayed width
Layout shift fixwidth and height attributes, or a CSS aspect-ratio
PriceFree, no account needed

Frequently asked questions

How do images affect Largest Contentful Paint?

On most pages the LCP element is an image, so the metric is essentially the time until that image finishes downloading and rendering. Its size in bytes therefore sets a floor no loading strategy gets under, which is why converting to WebP or AVIF and resizing to the displayed dimensions are the two highest-leverage changes available.

Should I lazy load all my images?

No — lazy load everything below the fold and nothing above it. A lazily-loaded LCP image cannot start downloading until the browser has done enough layout work to know it is visible, which delays exactly the request you needed earliest. Applying loading="lazy" to every image is a common change that makes LCP measurably worse.

How do I stop images causing layout shift?

Set the width and height attributes on every image element with the file's real pixel dimensions. Browsers use them to reserve correctly proportioned space before the image arrives, which removes the reflow entirely. They do not conflict with responsive CSS, and where dimensions are unknown in advance an explicit CSS aspect-ratio does the same job.

What image format is best for Core Web Vitals?

AVIF where you can and WebP as the baseline, with JPEG as a fallback for anything that needs it. WebP is roughly 25 to 35% smaller than JPEG at matching quality and AVIF saves more again, and both are supported by every current browser — so this is one of the few performance changes with no visible trade-off.

Does using fetchpriority actually help?

Yes, on the LCP image, typically by a couple of hundred milliseconds. A browser cannot tell which image is the LCP element until it has laid the page out, so fetchpriority="high" gives it that information up front. Use it on exactly one element per page — applying it broadly removes the prioritisation it was meant to create.

Should I preload my hero image?

Only if its URL is not discoverable in the initial HTML — for example it comes from CSS or is set by JavaScript. In that case preloading recovers the time the browser would spend finding it. If the image is a plain element in server-rendered HTML the preload scanner finds it immediately and a preload link adds nothing while competing with other resources.

Why hasn't my Core Web Vitals score improved after optimising images?

Field data aggregates 28 days of real visits, so a fix deployed today is still averaged against four weeks of the previous behaviour. Confirm with a lab tool that the change did what you intended, then wait. Also check which element is actually reported as LCP — it is often not the image you assumed.

Sources and further reading

More guides