A page's largest visible element is a hero photo applied with a CSS background-image on a <div>. Why does that usually produce a later Largest Contentful Paint (LCP) than the same photo placed in an <img> element, and what would you change?
answer
- who finds the URL first
- preload scanner reads markup, not CSS
- stylesheet must parse before the div matches
- load delay, not load duration
- no srcset or priority hint on a background
basics
~20 sA CSS background image is discovered only after the stylesheet downloads, parses and matches the element, so its request starts much later than an <img> the preload scanner finds in the raw HTML. Put the LCP photo in an <img>.
solid answer
~50 sBoth are valid LCP candidates — a CSS `background-image` can be the LCP element — so the problem is discovery timing, not eligibility. An `<img src>` sitting in the initial HTML is spotted by the preload scanner while the parser is still working, so the fetch starts within milliseconds of the first HTML bytes. A background image's URL lives inside CSS: the browser has to fetch and parse the stylesheet, build style, and work out that the `.hero` div actually renders and matches that rule before it can even request the file. In an LCP breakdown that shows up as a large *resource load delay* rather than a slow download. The fix is to make the photo real markup — an `<img>` positioned behind the text with `object-fit: cover` — which also buys you `srcset`, `fetchpriority` and the loading attributes that a background can never carry.
go deeper
Know that the browser can only request an image once it has seen its URL, and that a URL written in CSS is seen later than one written in the HTML. Say plainly that the main hero belongs in an <img>.
Be ready to walk the chain: HTML bytes, preload scanner, stylesheet fetch and parse, style and layout matching, then the image request. Name load delay as the span that grows, and list what a background image cannot carry.
Show the diagnosis, not just the rule: read a waterfall or Resource Timing, separate the gap before the request from the length of the request, and pick the fix that matches. Say when a background image is still the right call.
Own the systemic angle — how templates, CMS blocks and design-system hero components make this mistake repeatable, and what guardrail (a component that owns the hero image, or a budget check on LCP load delay) prevents it returning after the one-off fix.
## What LCP is actually timing Largest Contentful Paint records the moment the largest contentful element in the initial viewport is painted. The candidate set is broader than people expect: `<img>`, an `<image>` inside SVG, a `<video>` element's poster image, block-level text nodes, and any element whose computed style carries a `background-image: url(...)`. So a hero delivered as a CSS background is *not* disqualified from being the LCP element. It is still measured — it just gets measured later. The useful mental model is that LCP for an image decomposes into four spans: time to first byte, **resource load delay** (HTML arrived, but the image request had not started yet), resource load duration (the actual transfer), and element render delay (bytes are here, but the pixels are not on screen yet). A background hero almost always blows up the second span, and no amount of image compression touches it. ## Two very different discovery paths When the browser receives HTML it runs two things over those bytes: the main parser, which builds the DOM and can be blocked by a synchronous script, and a **preload scanner**, a lightweight look-ahead that reads the raw markup and immediately kicks off fetches for URLs it can see in attributes like `src` and `href`. An `<img src="/hero.jpg">` in the initial response is therefore usually requested in the same round trip as the CSS. A background image is invisible to that scanner, because the scanner reads markup, not stylesheets. The chain is: 1. Fetch the HTML, find `<link rel="stylesheet">`. 2. Fetch the stylesheet — render-blocking, so nothing paints in the meantime. 3. Parse it into the CSSOM. 4. Run style and layout far enough to know that a `.hero` element exists, is rendered (not `display: none`), and matches the rule. 5. *Only now* request the image. Every one of those steps is serialized. If the rule lives in a CSS chunk that a JavaScript bundle loads, or the `.hero` div is injected by client-side code, you add the whole download-plus-execute cost of that bundle in front of the image request. It is routine to see the hero request start a second or more after the HTML response on a mid-range phone. ```html <!-- discovered by the preload scanner in the first HTML chunk --> <img src="/hero-1600.jpg" width="1600" height="900" alt=""> <!-- discovered only after CSS is fetched, parsed and matched --> <div class="hero"></div> <style>.hero { background-image: url("/hero-1600.jpg"); }</style> ``` ## What else you give up Discovery is the headline cost, but a background image also loses the whole toolbox that lives on the element: - **No `srcset`/`sizes`.** CSS `image-set()` covers resolution switching but is far weaker than a width-descriptor `srcset`, so you tend to ship one oversized file to everyone. - **No priority hint.** In Chrome, images start at a low fetch priority and are only boosted once layout proves they are in the viewport; you cannot attach a priority attribute to a CSS URL. - **No per-element loading or decoding control.** - **Odd conditional behaviour.** A background image on an element that is not rendered is never fetched at all, which makes carousels and responsive show/hide patterns behave unpredictably. ## What to change Make the hero real markup. The usual pattern is an `<img>` with intrinsic dimensions, absolutely positioned inside the hero container with `object-fit: cover`, and the copy layered above it; gradients and overlays stay in CSS where they belong. That single change moves the request into the preload scanner's path and unlocks responsive candidates and priority signalling. If you genuinely cannot change the markup — a third-party template, a CMS-owned block — the escape hatch is to tell the browser about the URL early from the document head, which removes the load-delay penalty without moving the image. It only works when the URL is known at document-build time, so it fails for hero images chosen at runtime. Verify rather than assume: in a network waterfall or Resource Timing, compare the HTML response end with the image's request start. A long flat gap there is a discovery problem; a long bar is a transfer problem. They have completely different fixes, and teams routinely spend a sprint compressing an image whose real defect was that nobody asked for it until second three. ## When a background image is fine Decorative textures, section backgrounds below the fold, and anything that is not a plausible LCP candidate can stay in CSS — the late request costs nothing the user perceives. The rule is narrow and specific: the element most likely to *be* the LCP element belongs in markup.
- How would you prove the delay is discovery rather than a slow transfer?Compare the HTML response end with the image's request start, in a network waterfall or via Resource Timing. A long flat gap before the request begins is load delay — a discovery problem. A long bar once the request starts is transfer — a bytes or connection problem. Compressing the file only helps the second case.
- The design needs text on top of the photo. How do you keep it as an <img>?Put the `<img>` inside the hero container, absolutely positioned to fill it, with `object-fit: cover` and `alt=""` since it is decorative; give the container `position: relative` and stack the copy above it. Gradients and scrims stay as CSS layers. You keep preload-scanner discovery plus responsive candidates.
- Does an element's background image still count as the LCP element if it is huge but mostly covered by an overlay?Yes — LCP looks at the element's visible size in the viewport, not at aesthetic prominence, and a translucent overlay does not remove the underlying paint. That is exactly why full-bleed background heroes so often turn out to be the LCP element even though the team was watching the headline.
saying these in an interview costs you the question
- Says a CSS background image can never be the LCP element
- Blames file size when the real gap is discovery time
- Thinks the preload scanner reads stylesheets too
- Claims a priority attribute on the div would fix it
- Suggests lazy-loading the hero to improve LCP