On most sites, the LCP element is an image. It is also the part of the frontend that is most commonly mishandled — wrong format, wrong size, lazy when it should be eager, eager when it should be lazy, missing width and height attributes that cause layout shift the moment the bytes arrive. None of this is hard. It is just a small set of rules that need to be applied consistently.
The format ladder
Image formats are not interchangeable. For a typical photograph rendered at 1600px wide, the file sizes look roughly like this:
- JPEG — the baseline. A quality-80 JPEG at 1600px is around 200–400KB depending on subject. Universally supported, no transparency.
- WebP — typically 25–35% smaller than JPEG at visually identical quality. Same image lands around 140–280KB. Supported in every browser shipped after early 2020.
- AVIF — typically 45–55% smaller than JPEG, the same hero often lands under 120KB. Slower to encode, supported in Chrome, Firefox, and Safari 16+.
- PNG — only for graphics with hard edges, logos, or required transparency. A photographic PNG is two to four times larger than the JPEG equivalent.
- SVG — only for vector content. Logos, icons, simple illustrations. Never for photographs.
Serve the best format the browser accepts with the <picture> element:
<picture>
<source type="image/avif" srcset="/hero-1600.avif" />
<source type="image/webp" srcset="/hero-1600.webp" />
<img
src="/hero-1600.jpg"
alt="Team working in a shared editor"
width="1600"
height="900"
/>
</picture>The browser walks the sources top to bottom and uses the first one it can decode. The <img> is the fallback for browsers that ignore <picture> entirely, which is now a vanishingly small share.
The sizing strategy
Serving a 4000px image to a 800px viewport wastes bytes and CPU. The fix is srcset plus sizes — you supply the browser with several widths and a hint about which one it should pick:
<img
src="/hero-1200.jpg"
srcset="
/hero-480.jpg 480w,
/hero-768.jpg 768w,
/hero-1200.jpg 1200w,
/hero-1920.jpg 1920w
"
sizes="
(max-width: 768px) 100vw,
(max-width: 1200px) 90vw,
1200px
"
alt="..."
width="1200"
height="675"
/>Three to four widths is the right number for most images. Match them to your common breakpoints — a mobile width (480 or 640), a tablet width (768 or 1024), and one or two desktop widths. The sizes attribute tells the browser the rendered width at each breakpoint so it can pick the smallest source that still looks crisp at the device pixel ratio.
Always set width and height attributes that match the image's natural aspect ratio. The numbers themselves do not have to match the rendered size — the browser uses the ratio to reserve space before the image loads. Without them, CLS jumps every time an image arrives after layout.
Lazy loading rules
Native loading="lazy" is supported in every modern browser. It defers the network request for an image until the browser thinks the user is likely to scroll to it. The rules for using it are short:
- Below the fold: lazy. Almost every image on a blog post, a product listing page, or a long marketing page should have
loading="lazy". - Above the fold, but not LCP: eager. The default. No attribute needed.
- The LCP image: never lazy. Lazy-loading the LCP element forces the browser to wait for layout before it knows to fetch, which delays the request by hundreds of milliseconds.
- Always: width and height attributes. Lazy loading without dimensions is the most common cause of CLS regressions.
The LCP image is special
The single image that ends up as your Largest Contentful Paint element deserves its own treatment. Three moves push LCP down by hundreds of milliseconds each:
<head>
<link
rel="preload"
as="image"
href="/hero-1200.webp"
imagesrcset="/hero-768.webp 768w, /hero-1200.webp 1200w"
imagesizes="(max-width: 768px) 100vw, 1200px"
type="image/webp"
/>
</head>
<body>
<img
src="/hero-1200.webp"
srcset="..."
sizes="..."
alt="..."
width="1200"
height="675"
fetchpriority="high"
/>
</body><link rel="preload">tells the browser to start the image fetch as soon as the HTML is parsed, without waiting for the body to be reached.fetchpriority="high"bumps the request to the front of the queue, ahead of other images and async scripts.- No
loading="lazy". Ever. Even if you are using a component library that defaults to lazy — override it for this one image.
Hosting and CDN
An image CDN handles format negotiation, on-the-fly resizing, and caching automatically. Cloudinary, ImageKit, Imgix, and the built-in pipelines from Vercel and Netlify all do the same job: you upload one source image, the CDN serves the right format and size based on the request headers. Next.js's <Image> component wires up to whichever pipeline your host provides.
The trade-off is vendor lock-in and bandwidth cost. Vercel meters image optimisation by source image and transformed image volume, and a popular site can run a four-figure monthly bill before it notices. For self-hosted sites, building the responsive variants at deploy time with sharp and serving them from a plain CDN is usually cheaper and just as fast.
What real numbers look like
On a marketing page we audited last month, the hero was a single 1.2MB JPEG served at 1920px regardless of viewport. Mobile LCP on a 4G profile was 4.2s. The changes were unremarkable:
- Wrapped the hero in
<picture>with WebP and AVIF sources. JPEG fallback retained. - Generated four widths (480, 768, 1200, 1920) with
sharpat build time. Hero now serves a 78KB AVIF at 1200px on desktop and a 22KB AVIF at 480px on mobile. - Added
<link rel="preload">for the hero andfetchpriority="high"on the img. - Added
loading="lazy"to every other image on the page (the testimonial photos, the feature illustrations, the footer logos).
Mobile LCP dropped from 4.2s to 1.9s on the same 4G profile. Total image bytes on first paint went from 1.2MB to 22KB. The CSS, JavaScript, and HTML didn't change at all.
The habit that compounds
Image work is the closest thing to a free performance win that exists in frontend engineering. The rules are short, the tools are mature, and the wins are measured in seconds rather than tens of milliseconds. The habit that compounds is auditing the LCP image on every new template before it ships — five minutes of checking format, size, preload, and lazy state — because the alternative is finding the regression in production three weeks later when a field-data dashboard flags it.
Related reading
For the metric definitions, start with Core Web Vitals explained. The companion post on font loading without layout shift covers the other big asset class. To find out whether your changes are actually moving the field number, set up real-user monitoring. The full performance silo is on the web performance topic page.
About the writers
Developer educator at ShareCode. Writes the tutorial track — Python, JavaScript debugging, coding-interview prep, and the everyday code-quality habits that hold up in real codebases.
More from Kajal
Founder of ShareCode. Writes the engineering deep-dives on this site — WebRTC, Firebase Auth, real-time sync, and the production patterns behind the editor itself.
More from Kishan
Auditing your hero image?
Paste the markup and the asset sizes into a code space and walk through the format ladder, srcset, and preload checklist with a teammate. Most hero-image audits surface a ten-times bandwidth saving in the first pass.
Open a code space →