Guide
Image Optimization for Core Web Vitals, Measured
Image optimization for Core Web Vitals touches two metrics. For LCP, serve the hero as AVIF at the width it renders, in the initial HTML, with fetchpriority="high" on that one image. For CLS, give every image width and height attributes.
We measured both instead of repeating the usual advice. At equal perceptual quality, AVIF came out 23 to 39% smaller than a well-encoded JPEG on four standard test photos, 27% smaller on a 1,600-pixel hero and 66% smaller on a UI screenshot. WebP, the format most guides still lead with, ranged from 23% smaller than the same JPEG to 3% larger, depending on the photo. And one image without dimensions produced a CLS of 0.158 on a desktop viewport, enough on its own to fail Google’s 0.1 “good” line.
Everything below was measured on 27 September 2026 on an Apple M2 Pro MacBook Pro running macOS 27.0, with the encoder versions and test images named in each section. We build Smol, a Mac compression app, and the last section is specific about where it does not belong in this workflow.
Which Core Web Vitals do images actually affect?
Two of the three, and not equally.
| Metric | Good / poor (75th percentile) | How images move it | The fix |
|---|---|---|---|
| LCP | ≤ 2.5 s / over 4.0 s | The hero is usually the LCP element, so its bytes and how early the browser finds it set the score | Right format, right width, in the HTML, one high-priority fetch |
| CLS | ≤ 0.1 / over 0.25 | An image with no reserved space pushes the page down when it arrives | width and height attributes, or CSS aspect-ratio |
| INP | ≤ 200 ms / over 500 ms | Rarely. INP is mostly a JavaScript problem | decoding="async" on images a script swaps in after a click |
The thresholds are Google’s, from web.dev’s pages on LCP, CLS and INP. The LCP row decides most pages. The HTTP Archive Web Almanac 2025 found an image is the LCP element on 85.3% of desktop pages and 76.0% of mobile pages, from its July 2025 crawl, and only 62% of sites have a good LCP on phones.
The CLS row is the embarrassing one, because the fix is two attributes. The same chapter found 62% of mobile pages and 65% of desktop pages still ship at least one image without explicit dimensions.
INP gets one sentence because it deserves one. The only image case we would worry about is a gallery or lightbox that swaps in a large original on click; MDN documents that decoding="async" means “the next paint does not wait for the image to decode.”
AVIF, WebP or JPEG: which is smallest at the same quality?
AVIF, by a wider margin than the WebP-versus-JPEG argument most guides are still having. First the method, because comparing formats at the same quality setting is meaningless: quality 80 in one encoder and quality 80 in another are unrelated numbers.
We encoded each source at every quality setting from 20 to 100 in all three formats, decoded each result, and scored it against the lossless master with SSIMULACRA2, the perceptual metric from the JPEG XL project. The tool’s own usage text anchors 70 as “high quality” and 90 as “very high quality,” impossible to distinguish from the original at 1:1. For each format we kept the smallest file scoring at least 70. Encoders were mozjpeg 4.1.5, libwebp 1.6.0 at -m 6 and libavif 1.4.1 with aom at speed 6, otherwise at their defaults.
The sources: four photos from the Kodak Lossless True Color Image Suite, the standard compression test set, which its archive describes as released by Kodak for unrestricted usage; a 1,600-pixel hero downscaled from Giles Laurent’s photo of Château de Chillon (CC BY-SA 4.0); and a dashboard screenshot we built in HTML and rendered with Chrome at 2×.
| Source (pixels) | JPEG (mozjpeg) | WebP | AVIF | AVIF vs JPEG |
|---|---|---|---|---|
| Kodak kodim03, caps (768 × 512) | 35,672 B | 27,516 B | 22,076 B | −38.1% |
| Kodak kodim08, houses (768 × 512) | 76,683 B | 70,746 B | 55,803 B | −27.2% |
| Kodak kodim19, lighthouse (512 × 768) | 50,246 B | 51,554 B | 38,845 B | −22.7% |
| Kodak kodim23, parrots (768 × 512) | 29,098 B | 25,386 B | 17,791 B | −38.9% |
| Hero photo (1,600 × 1,067) | 187,523 B | 183,746 B | 137,232 B | −26.8% |
| Dashboard screenshot (1,600 × 1,000) | 48,629 B | 30,094 B | 16,706 B | −65.6% |
Three things fall out of that table.
WebP is an inconsistent upgrade on photographs. Against mozjpeg it was 23% smaller on the saturated caps, 2% smaller on the hero and 3% larger on the lighthouse, for 8.6% across the four Kodak photos. The encoder you compare against matters as much as the format: rerun with libjpeg-turbo 3.2.0 instead of mozjpeg and JPEG needed 7 to 11% more bytes for the same score, which is a large share of the gap WebP is usually credited with.
AVIF wins on every file, and hardest on flat UI. It was 30% smaller than JPEG across the Kodak four, 27% on the hero and 66% on the screenshot, and 20 to 45% smaller than WebP on all six sources.
At the top end, WebP runs out of headroom. Raise the target to 90 and lossy WebP could not reach it on four of the six sources even at quality 100: it topped out at 84.2 on the hero and 81.8 on the screenshot. Lossy WebP is a VP8 keyframe, and VP8 “works exclusively with an 8-bit YUV 4:2:0 image format”, so fine colored edges lose resolution whatever the quality slider says. AVIF reached 90 on all six; on the hero it needed 590,546 bytes where JPEG needed 1,018,062.
Screenshots have one more option. If a UI capture must be pixel-exact, lossless WebP came out at 44,288 bytes, verified identical to the source, against 90,157 for the same PNG crushed with oxipng -o max. Lossless AVIF was the worst of the three at 174,087 bytes. For a screenshot on a marketing page, lossy AVIF at score 70 was 16,706 bytes, 38% of the lossless WebP.
| The image is | Ship | Fallback in <picture> | Why |
|---|---|---|---|
| A photograph, hero or inline | AVIF | JPEG from mozjpeg | A WebP middle rung bought 2 to 23% over mozjpeg in our tests |
| A UI screenshot, chart or flat illustration | AVIF | PNG, or lossless WebP | Largest AVIF win we measured |
| Something that must be pixel-exact | Lossless WebP | PNG | Half an optimized PNG; lossless AVIF was bigger than the PNG |
| A logo, icon or line drawing | SVG | None needed | Not a raster problem |
| An animated GIF | MP4 or WebM video | None needed | GIF is the wrong container for motion |
AVIF is decoded by Chrome since version 85, Firefox since 93, Safari since 16.4 and Edge since 121, per caniuse, so in 2026 a WebP middle rung mostly serves browsers that are several years out of date. If you already ship WebP, keep it. If you are choosing today, AVIF plus a JPEG fallback is less work and fewer files. Our walkthroughs for converting PNG to WebP and turning GIFs into MP4 cover the one-off conversions, and the Mac compressor roundup compares the desktop tools that write these formats.
How much does serving the right width save?
More than the format does. Same hero, same encoder settings at every size (AVIF at quality 58 and mozjpeg at 79, the settings that reached score 70 at 1,600 pixels), five widths. Scores stayed between 70.2 and 72.5 at every rung.
| Width (pixels) | AVIF | JPEG (mozjpeg) |
|---|---|---|
| 400 × 267 | 11,327 B | 15,182 B |
| 800 × 534 | 38,416 B | 52,620 B |
| 1,200 × 800 | 81,128 B | 110,277 B |
| 1,600 × 1,067 | 137,232 B | 187,523 B |
| 2,400 × 1,601 | 283,756 B | 390,616 B |
Doubling the width cost 3.5 to 3.6 times the bytes, because bytes track pixel count, not width. Now the comparison that matters. A phone at 2× density showing this hero across 390 CSS pixels needs about 780 device pixels, which is the 800 rung: 38,416 bytes as AVIF. Send it the single 2,400-pixel JPEG that many sites serve to everyone and it downloads 390,616 bytes, ten times as much, for pixels it cannot display.
Even the right format at the wrong width loses. The 2,400-pixel AVIF is 5.4 times the size of the 800-pixel JPEG. If you have time for one change this week, fix the width.
The mechanism is srcset with width descriptors and an accurate sizes, which we walk through step by step in image optimization for web developers. Two details belong here. If you leave sizes off, MDN specifies a default of 100vw, so the browser assumes the image is full-bleed and picks a candidate to match. And for images below the fold there is now sizes="auto", which uses the laid-out width instead of your media conditions. MDN notes it is only valid together with loading="lazy", so it is never an option for the hero.
Why should only one image get fetchpriority="high"?
Because priority is relative. According to web.dev’s Fetch Priority guide, Chrome starts every image at Low and boosts the ones inside the viewport to High only once layout has run. Since Chrome 117, the first five images larger than 10,000 square pixels start at Medium instead. fetchpriority="high" lets the hero skip that wait and start at High as soon as the HTML parser sees it. Google reports the attribute moved LCP on a Google Flights test from 2.6 seconds to 1.9.
The same guide contains the reason not to spray it around: the browser downloads resources with the same computed priority in the order it discovers them. Mark six images high and you have rebuilt the queue you were trying to jump, with the hero competing against a logo and five carousel slides for the same early bandwidth. A page has one LCP candidate at a given viewport. That image gets the attribute.
The opposite attribute is underused. Carousel slides that start hidden can sit close enough to the viewport to get boosted anyway, so web.dev recommends fetchpriority="low" for them, and reports an experiment on the Oodle app where lowering those images’ priority cut page load time by 2 seconds.
Adoption is climbing. The Web Almanac 2025 counted fetchpriority="high" on the LCP image on 17.3% of mobile pages and 16.3% of desktop pages. It also found 0.3% of pages setting fetchpriority="low" on their LCP image, which is almost certainly an accident.
The mirror-image mistake is lazy-loading the hero, and 16 to 17% of pages still do it, per the same chapter: 10.4% of mobile pages through native loading="lazy" and 5.9% through scripts that hide the URL in data-src. When web.dev tested WordPress archive pages with lazy loading switched off, median LCP improved 13% on desktop (2,029 to 1,759 ms) and 15% on mobile (1,657 to 1,403 ms).
<!-- The one LCP image: in the HTML, eager, high priority, dimensioned -->
<picture>
<source type="image/avif"
srcset="/hero-800.avif 800w, /hero-1200.avif 1200w,
/hero-1600.avif 1600w, /hero-2400.avif 2400w"
sizes="(min-width: 1280px) 1200px, 100vw">
<img src="/hero-1600.jpg"
srcset="/hero-800.jpg 800w, /hero-1200.jpg 1200w,
/hero-1600.jpg 1600w, /hero-2400.jpg 2400w"
sizes="(min-width: 1280px) 1200px, 100vw"
width="1600" height="1067" fetchpriority="high" alt="…">
</picture>
<!-- Everything below the fold -->
<img src="/card-800.jpg"
srcset="/card-400.jpg 400w, /card-800.jpg 800w"
sizes="auto, (min-width: 640px) 380px, 100vw"
width="800" height="534" loading="lazy" decoding="async" alt="…">fetchpriority, width and height go on the <img>, not on the <source> elements. The hero carries no loading attribute at all; the default is eager, and writing it out invites someone to “tidy” it into lazy later.
When should you preload the hero image?
Only when the browser cannot see it in the HTML. web.dev’s LCP guide lists the cases: an <img> added by JavaScript, a lazy-loading library that hides the URL in data-src, and a CSS background image. In all three the browser must fetch and run something else before it learns the image exists.
If the hero is a plain <img> or <picture> in the served HTML, do not preload it. web.dev’s page on responsive preloads is explicit: “For sites where the image is quickly discoverable in the HTML, we recommend avoiding preload,” because the preload scanner already finds it, and a separate preload risks “becoming outdated from the main markup when images or pages change.” An outdated preload is worse than none. It is a second download of a file the page never shows.
When you do need one, two rules. Put fetchpriority="high" on the <link>, because an image preload otherwise gets the default priority for images, which is low. And for a responsive image, use imagesrcset and imagesizes with exactly the values the element uses, so the browser preloads the candidate it will later choose.
<!-- Only for a CSS-background or script-rendered hero -->
<link rel="preload" as="image" type="image/avif" fetchpriority="high"
imagesrcset="/hero-800.avif 800w, /hero-1600.avif 1600w, /hero-2400.avif 2400w"
imagesizes="100vw">Only 2.1% of mobile pages preload their LCP image, according to the Almanac. For once the low number is roughly right: most pages should not need to.
What does one image without width and height do to CLS?
Enough to fail the metric by itself. We built a plain article page (a heading, one line of text, a photo, then eight paragraphs) and served it from a local server that held the photo back for 800 ms, so the text painted first, as it does on a real connection. Chrome 153 ran headless, read layout-shift entries through a PerformanceObserver, and we ran each case three times.
| Viewport | Image rendered at | No width / height | With width / height |
|---|---|---|---|
| Desktop, 1,280 × 800 | 720 × 480 | 0.158 | 0 |
| Phone, 390 × 844 at 3× | 342 × 228 | 0.184 | 0 |
Every run of each case returned the identical score. Both unsized results are over the 0.1 “good” threshold from a single photo, before ads, web fonts or a cookie banner have added anything.
The arithmetic explains why a small phone image does as much damage as a big desktop one. web.dev defines each layout shift as impact fraction × distance fraction, where the distance is how far content moved relative to the viewport’s larger side. On desktop the text dropped 480 pixels in a viewport whose larger side is 1,280, a distance fraction of 0.375. On the phone it dropped only 228 pixels, but nearly all of that small screen was text that moved, so the impact fraction was far higher: about 0.68 against 0.42 on desktop, derived from the measured scores.
The fix is the most boring line in this article. Put the image’s intrinsic width and height on the <img> and keep height: auto in CSS so it still scales. The browser derives the aspect ratio from the two attributes and reserves the box before a byte arrives, which is exactly what took both scores to zero. For an image whose shape you only learn at runtime, CSS aspect-ratio on its container does the same job.
<img src="/photo.jpg" width="1600" height="1067" alt="…">
img { max-width: 100%; height: auto; }Where does a desktop app fit, and when is Smol the wrong tool?
Most of what this article recommends is markup, and no compression app writes your markup. Smol will not add width and height to your templates, choose your sizes, or decide which image gets fetchpriority. Those are code changes, and the CLS and priority wins above live entirely in them.
If your images go through a build step, use the build step. An image pipeline in CI or an image CDN is the right tool for anything committed to a repository or uploaded by users, and a Mac app has no place in either path. If your site runs WordPress, a plugin that converts on upload is a better fit than any desktop tool; our ShortPixel comparison says so in detail. If you work on Linux or in containers, Smol is macOS only.
Where a local tool earns its place is the bytes before they reach the repository: the 12 MB hero from a photographer, the folder of product shots from a client, the screenshots for a docs page. We covered that failure in compressing images before a git commit. Smol writes AVIF, WebP, JPEG, PNG, HEIC and GIF, exposes quality from 0 to 100, and resizes by fit, fill, width or height, so the 800, 1,200, 1,600 and 2,400-pixel rungs are one pass per width. Its defaults are quality 75 inside a 2,000-pixel box with metadata stripped. Its quality scale is its own, like every encoder’s, so do not expect Smol’s 58 to equal avifenc’s 58; check the output against your target instead of borrowing a number from this page.
If an AI coding agent already edits your front end, Smol’s MCP server lets Claude Code, Codex or Google Antigravity generate those rungs as a tool call and write the <picture> block in the same step. Its neural mode also takes a byte budget between 4 and 4,096 KB, for when a performance budget is the real constraint.
Smol is a one-time $29 purchase for one Mac. For the adjacent jobs there is compressing images for the web and converting to AVIF on a Mac.
Frequently asked questions
Does image optimization actually improve Core Web Vitals?
Yes, for two of the three metrics. An image is the LCP element on 85.3% of desktop pages and 76.0% of mobile pages in the Web Almanac 2025 data, so hero bytes and discovery time decide LCP on most sites. Images without width and height cause layout shift: one unsized photo scored CLS 0.158 in our desktop test. INP is rarely affected by images.
Is AVIF better than WebP for an LCP image?
In our measurements, yes. At the same SSIMULACRA2 score of 70, AVIF was 20 to 45% smaller than WebP on all six test images, while WebP ranged from 23% smaller to 3% larger than a mozjpeg JPEG on photographs. Chrome 85+, Firefox 93+, Safari 16.4+ and Edge 121+ decode AVIF, so serve it with a JPEG fallback inside a picture element.
Should every above-the-fold image get fetchpriority="high"?
No. Priority is relative, and the browser downloads resources with the same priority in the order it finds them, so marking several images high recreates the queue you wanted to skip. Put fetchpriority="high" on the single image most likely to be the LCP element, and consider fetchpriority="low" for carousel slides that are hidden when the page loads.
Do I need to preload my hero image?
Usually not. If the hero is an img or picture element in the server-rendered HTML, the preload scanner finds it immediately, and web.dev recommends avoiding preload in that case. Preload only when the image is a CSS background, rendered by JavaScript, or hidden behind data-src, and add fetchpriority="high" to the preload, because image preloads otherwise default to low priority.
How do width and height attributes prevent layout shift?
The browser uses the two attributes to work out the image’s aspect ratio and reserves its box before the file arrives. Pair them with height: auto in CSS so the image still scales with the layout. In our test, adding them took CLS from 0.158 to 0 on a 1,280 × 800 desktop viewport and from 0.184 to 0 on a 390 × 844 phone viewport.
Should I lazy-load images to improve LCP?
Only images below the fold. Lazy-loading the LCP image delays it: when web.dev tested WordPress archive pages with lazy loading switched off, median LCP improved 13% on desktop and 15% on mobile. The Web Almanac 2025 still found 16 to 17% of pages lazy-loading their LCP image, most of them through the native loading="lazy" attribute.
Keep reading