The short answer: look at which part is large first
When image LCP is slow the first reflex is to shrink the image or convert it to WebP or AVIF. Sometimes that is the right move, but web.dev's LCP guide says LCP time splits into four subparts and that the resource load duration, the time the file takes to download, is not a significant bottleneck for most sites. So diagnosis comes first: is the time spent on the server, in discovering the image late, in downloading it or in painting it?
This article continues the image budget idea in a fast web experience. That article has a section on setting an image budget; here we focus on one indicator, image LCP, and on separating the causes that slow it: request discovery, size and ratio, and format.
Last checked: 25 October 2026. The numbers in the article come from two places: thresholds and percentages quoted from web.dev and MDN, and the synthetic waterfall values that are explicitly marked as a "design example". I make no claim about an external user site or a real performance gain, and I did not take a real browser measurement for this article.
LCP and its four subparts
LCP, Largest Contentful Paint, is the render time of the largest image, text block or video visible in the viewport. According to web.dev, eligible elements include img elements, image elements inside an svg, video poster images, background images loaded with url() and block-level elements containing text. A good LCP is 2.5 seconds or less for 75% of visits; more than 4.0 seconds counts as poor. The measurement looks at the 75th percentile of page loads, segmented across mobile and desktop.
Google Search's Core Web Vitals document gives the same LCP target (within the first 2.5 seconds), an INP below 200 milliseconds and a CLS below 0.1, and says these metrics, along with other page experience aspects, align with what the core ranking systems seek to reward. That is not a ranking guarantee, only a target.
web.dev splits LCP time into four subparts. This split is the backbone of the diagnosis.
| Subpart | Definition per web.dev | First suspect when it is large |
|---|---|---|
| TTFB | The time from when the user starts loading the page until the browser receives the first byte of the HTML response. | Server response, redirects, cache. |
| Resource load delay | The time between TTFB and when the browser starts loading the LCP resource. | The image appearing late or never in the HTML, loading="lazy", data-src. |
| Resource load duration | The time it takes to load the LCP resource itself. | File size, format, distance, cache. |
| Element render delay | The time between the LCP resource finishing loading and the LCP element rendering fully. | Everything that happens after the download; this article does not examine the causes separately. |
The definitions come from web.dev's LCP optimization guide. For the ideal split the guide gives, as guidelines, roughly 40% TTFB, under 10% resource load delay, roughly 40% resource load duration and under 10% element render delay, and tells readers not to convert them into absolute numbers.
The lab and field difference matters here too. web.dev writes that LCP includes unload time, connection setup, redirects and TTFB delays, that these delays can be significant in the field and that lab and field measurements can differ. So the result of a single lab run is not the same thing as the 75th percentile of real visitors.
Reading a waterfall: late discovery and a long download are different diagnoses
The two bars above show two different faces of the same four parts. In example A the resource load delay is large: the browser finds the image late. In example B the resource load duration is large: the browser finds the image early, but the file takes long to arrive. The fixes differ. In the first, shrinking the file does not solve the problem; in the second, adding a preload alone is not enough. The numbers are a synthetic design example, not a measurement.
You can do the same calculation with a small script. For each scenario it derives the subparts from four timestamps (TTFB, when the resource starts loading, when it finishes and the LCP moment), rounds their shares to percentages and prints the largest part after TTFB. Because the shares are rounded, they may not add up to exactly 100.
01SCENARIOS = {02 "A": {"ttfb": 600, "resource_start": 1500, "resource_end": 2000, "lcp": 2100},03 "B": {"ttfb": 600, "resource_start": 700, "resource_end": 2200, "lcp": 2300},04}05 06for name, t in SCENARIOS.items():07 parts = {08 "ttfb": t["ttfb"],09 "load_delay": t["resource_start"] - t["ttfb"],10 "load_duration": t["resource_end"] - t["resource_start"],11 "render_delay": t["lcp"] - t["resource_end"],12 }13 assert sum(parts.values()) == t["lcp"]14 share = {key: round(100 * value / t["lcp"]) for key, value in parts.items()}15 after_ttfb = {key: value for key, value in parts.items() if key != "ttfb"}16 print(name, parts, "total", t["lcp"])17 print(" share", share)18 print(" load_delay_share_over_10", share["load_delay"] > 10, "render_delay_share_over_10", share["render_delay"] > 10)19 print(" largest_after_ttfb", max(after_ttfb, key=after_ttfb.get))20 01A {'ttfb': 600, 'load_delay': 900, 'load_duration': 500, 'render_delay': 100} total 210002 share {'ttfb': 29, 'load_delay': 43, 'load_duration': 24, 'render_delay': 5}03 load_delay_share_over_10 True render_delay_share_over_10 False04 largest_after_ttfb load_delay05B {'ttfb': 600, 'load_delay': 100, 'load_duration': 1500, 'render_delay': 100} total 230006 share {'ttfb': 26, 'load_delay': 4, 'load_duration': 65, 'render_delay': 4}07 load_delay_share_over_10 False render_delay_share_over_10 False08 largest_after_ttfb load_durationIn scenario A the resource load delay share is 43%. web.dev's guideline suggests this part should be under 10%, so A is an example where late discovery is the first suspect. In B the delay is only 4% and the largest part is the resource load duration, that is, the download. Do not use the total to rank the two: A totals 2100 ms and B 2300 ms, but the diagnosis comes from which part is large, not from the total.
One more warning: the 2.5-second threshold is defined for the 75th percentile of visits. Even though these examples total less than 2.5 seconds, that does not mean "good"; a single run is not a percentile. The example only shows how to separate the subparts.
Request discovery: the image must be visible in the HTML and must not be delayed
The foundation of reducing resource load delay is that the LCP image can be discovered in the initial HTML response. web.dev lists the cases where it cannot: an img added with JavaScript, images loaded late through data-src and CSS background images. MDN explains why: while a page loads, the browser starts downloading images before it begins interpreting the CSS and JavaScript, so the choice has to be declarative, that is, visible in the HTML.
According to MDN, loading="lazy" defers loading the image until it reaches a calculated distance from the viewport. That helps for images further down the page and is the opposite of what the LCP image needs. web.dev says "never lazy-load your LCP image", because it always leads to unnecessary resource load delay.
According to MDN, fetchpriority="high" is a hint: it asks for the image to be fetched at a high priority relative to other images. web.dev writes that it is a good idea on an img that is likely to be the LCP element, but that setting a high priority on more than one or two images makes priority setting unhelpful. Images that are early but not visible (for example the later slides of a carousel) can get fetchpriority="low".
Preload is a conditional tool. web.dev recommends preload when the image is only referenced from an external CSS or JavaScript file; if the image already appears in the HTML as an img, the browser discovers it early. MDN discourages preloading several types of the same resource: do it for the type the majority will use. Preload schedules a file to be downloaded; preloading the wrong file downloads the wrong file.
The script below scans the img elements of a synthetic HTML page and flags these reasons: the image not appearing in the HTML, loading="lazy", a missing width or height, a width-descriptor srcset without sizes (the default 100vw applies, according to MDN) and the count of fetchpriority="high". It is a heuristic: the first image in the HTML may be an LCP candidate, but the real LCP element is only known after the page has rendered.
01import re02from html.parser import HTMLParser03 04PAGE = """<body>05<img src="/img/hero-1600.jpg" loading="lazy" alt="Hero" width="1600" height="800">06<img data-src="/img/banner.jpg" alt="Banner">07<img src="/img/photo-800.avif" srcset="/img/photo-800.avif 800w, /img/photo-1600.avif 1600w"08 sizes="(width <= 600px) 100vw, 800px" width="800" height="450" fetchpriority="high" alt="Photo">09<img src="/img/thumb.jpg" srcset="/img/thumb-400.jpg 400w, /img/thumb-800.jpg 800w" width="400" alt="Thumb">10</body>"""11 12 13class Images(HTMLParser):14 def __init__(self):15 super().__init__()16 self.images = []17 18 def handle_starttag(self, tag, attrs):19 if tag == "img":20 self.images.append(dict(attrs))21 22 23parser = Images()24parser.feed(PAGE)25high = 026for number, img in enumerate(parser.images, 1):27 flags = []28 if "src" not in img and "srcset" not in img:29 flags.append("not_discoverable_in_html")30 if img.get("loading") == "lazy":31 flags.append("lazy")32 if "width" not in img or "height" not in img:33 flags.append("no_dimensions")34 if re.search(r"\d+w\b", img.get("srcset", "")) and "sizes" not in img:35 flags.append("default_sizes_100vw")36 if "sizes" in img and "srcset" not in img:37 flags.append("sizes_without_srcset")38 high += img.get("fetchpriority") == "high"39 print(number, img.get("src") or img.get("data-src"), ",".join(flags) or "ok")40print("fetchpriority_high", high)41 011 /img/hero-1600.jpg lazy022 /img/banner.jpg not_discoverable_in_html,no_dimensions033 /img/photo-800.avif ok044 /img/thumb.jpg no_dimensions,default_sizes_100vw05fetchpriority_high 1In the output the first image is lazy-loaded: if it sits at the top of the page it may be an LCP candidate and causes resource load delay. The second image has no src; its address is only in data-src and cannot be discovered from the HTML. The third image is clean. The fourth has no height and has a width-descriptor srcset but no sizes. The script does not tell you the fix; it shows which image to look at.
Size and ratio: srcset, sizes, width and height
Three decisions are made while the image is still HTML: which file (size), how much room (ratio) and which format. The diagram below puts the three side by side.
srcset and sizes work together. According to MDN, srcset defines the files the browser may choose from and their widths (like 800w, with the w unit, not px); sizes says how much room the image takes under which screen condition. The browser looks at screen size, pixel density, zoom level, orientation and network speed, uses the slot size of the first condition that is true and selects a suitable candidate while accounting for pixel density. Do not predict the choice just by comparing file width with CSS slot width; verify the loaded candidate in the Network panel. The browser makes the choice; you provide the options and the hints.
Two rules are often missed. The w value in srcset must match the file's intrinsic width, and sizes should only be used with width descriptors; mixing width and density (x) descriptors is wrong. If sizes is not given the default is 100vw, that is, the image is assumed to be as wide as the viewport; if the image actually sits small in a column the browser may pick a larger file than needed.
01<picture>02 <source type="image/avif"03 srcset="/img/hero-800.avif 800w, /img/hero-1600.avif 1600w"04 sizes="(width <= 800px) 100vw, 800px">05 <img src="/img/hero-800.jpg"06 srcset="/img/hero-800.jpg 800w, /img/hero-1600.jpg 1600w"07 sizes="(width <= 800px) 100vw, 800px"08 width="800" height="450" fetchpriority="high"09 alt="Synthetic hero image">10</picture>In the example the source inside picture declares AVIF with type; a browser that does not support that type skips the source and moves on to the JPEG candidates in img. The img has width and height; because it is the hero it has no loading (the default is eager) and fetchpriority="high" is on a single image. The addresses are synthetic; this markup is not a template to copy without testing it on your page.
The width and height attributes give the image's intrinsic size. According to MDN they let the browser calculate the aspect ratio before the image loads; space is reserved and layout shift is reduced or prevented. web.dev recommends always including width and height on image and video elements, or using the CSS aspect-ratio; Chrome shows this in DevTools as something like aspect-ratio: auto 640 / 360. The good CLS threshold is 0.1 or less for 75% of visits. Ratio concerns CLS rather than LCP time directly; you still need to watch both metrics together.
Format: AVIF, WebP, PNG and JPEG are chosen by context
MDN's image format guide gives a simple principle: for raster images WebP or AVIF generally compress better than PNG, JPEG and GIF; for screenshots, diagrams, logos and line art lossless encoding is preferred because blurring and colored fringes around text and sharp edges are very visible; continuous-tone images such as photographs can usually tolerate lossy compression. The table below summarizes that guide.
| Format | Per MDN | When to consider it | Caution |
|---|---|---|---|
| AVIF | Lossy and lossless; lossy AVIF is described as around 50% smaller than JPEG. | Photographs and continuous-tone images; in browsers that support it. | It does not support progressive rendering, so the file must be fully downloaded before it can be shown; provide a fallback. |
| WebP | Lossy and lossless; lossy WebP is on average 25–35% smaller than JPEG, and lossless WebP is typically 26% smaller than PNG. | A general-purpose option with broad support. | It has no deep historical support; provide a JPEG or PNG fallback. |
| PNG | Lossless compression and full alpha transparency. | Screenshots, line art, precise reproduction, transparency. | AVIF and WebP offer better compression, but browser support is narrower. |
| JPEG | Lossy for photographs; no alpha channel. | A fallback for photographs. | Lossy compression can give poor results on sharp content such as diagrams and charts. |
| SVG | Recommended for diagrams and charts that must be drawn accurately at different sizes. | Diagrams, charts, logos. | It is a vector format; it does not replace a photograph. |
The percentages are claims on the MDN page; the median comparison of AVIF compressing better than WebP is attributed to a third-party test on a single set of JPGs. The result for your images may differ; measure your own files.
AVIF not supporting progressive rendering means the file cannot be shown before it has fully downloaded, so do not choose a format by file size alone. For a format fallback use picture and type: MDN suggests a PNG or JPEG fallback for browsers that do not support WebP, AVIF or JPEG XL. Browser support version lists change over time, so check MDN for the current list; this article gives no version numbers.
Converting WebP to PNG is not a speed or quality move. Detail lost in a lossy WebP file does not come back when it is converted to PNG; PNG is lossless but only stores the pixels you already have, which are already lossy. The file may also grow, so a speed expectation would be wrong. If a workflow or a tool asks for PNG, the WebP to PNG converter does the conversion in the browser; for a page's LCP image, first look at which of the four parts above is large.
To shrink an image the image compressor outputs JPG or WebP and compares the sizes before and after; do not count a result as a gain until you have measured it again on your own page. A share image (Open Graph) is a separate job from the LCP image: the Open Graph image generator creates an exact 1200×630 pixel image.
Measurement: lab, field and a single fixture
What supports a claim of improvement is a before-and-after measurement under the same conditions. There are two kinds of LCP data. Lab data is a single run in a controlled environment; field data comes from real visitors and is summarized at the 75th percentile. Google's Core Web Vitals document points to the Core Web Vitals report in Search Console; web.dev notes that lab and field can differ.
A single fixture, that is, one page, one device and one network condition, is only the result of that condition. "AVIF lowered LCP on this page" is true only under the conditions you measured; saying it is true for all users takes field data. There is no real measurement result anywhere in this article.
| Field | Why it is recorded |
|---|---|
| Browser and version | Behavior can change between versions; the result is read for that version only. |
| Fixture | Which page, which image, which content: before and after are compared on the same fixture. |
| Cache state | Cached and uncached loads can enlarge different subparts. |
| Throttle | Network and CPU limits shape a lab result; without the limit the number cannot be interpreted. |
| Repetition count | A single run contains noise; record the repetition count and the spread. |
| Result and interpretation limit | The result is interpreted only for these conditions; it is not presented as a general user result. |
Be careful about ranking too. According to Google's document Core Web Vitals, along with other page experience aspects, aligns with what the core ranking systems seek to reward; that does not mean an LCP improvement will raise rankings.
Limitations and a working checklist
The limits of this article are explicit. I did not take a real browser measurement; the waterfall numbers are a synthetic design example. The sources are web.dev, MDN and Google Search documentation; thresholds and format information are written as they stood on 25 October 2026. I did not verify topics such as CDNs, servers, INP, decoding and responsive preload, so I did not describe them. No item is a guarantee of speed, LCP or ranking. If you build a site with a tool such as Astro, its practical guide is a separate topic; this checklist looks at the HTML that is produced.
- You identified the LCP element from a measurement; you did not assume that the first image in the HTML is the LCP element.
- The LCP image appears in the initial HTML response as an
imgorsource; it is notdata-src, added with JavaScript or a CSS background. - The LCP image has no
loading="lazy". fetchpriority="high"is on at most one or two images.- Preload is used only when the resource is referenced from an external CSS or JavaScript file; there is no preload for several types of the same image.
- The
wvalues insrcsetmatch the file widths;sizesstates the image's real slot width. widthandheight(oraspect-ratio) are present; the ratio is known before the image loads.- The format was chosen by context and a fallback is provided with
picture; nobody assumes that making a PNG from a lossy WebP brings speed or detail. - Lab and field results are reported separately; a single-fixture result is not presented as a general user result.
Sources and check date
The pages below were read on 25 October 2026. Last checked: 25 October 2026.
- web.dev: Optimize Largest Contentful Paint
- web.dev: Largest Contentful Paint (LCP)
- web.dev: Optimize Cumulative Layout Shift
- Google Search Central: Understanding Core Web Vitals and Google Search results
- MDN: Responsive images
- MDN: The img element
- MDN: Image file type and format guide
- MDN: rel=preload
No real browser measurement was made for this article, and no external user site or real performance gain is claimed. The waterfall numbers are a synthetic design example. Thresholds, percentages and format information were taken from the pages above on 25 October 2026; those pages may have been updated since, so verify the current state there. Nothing in the article is a guarantee of speed, LCP or ranking.
Measure which part is large first, then make one change that targets that part.
Explore more articles ↗