Problem
If you've ever run Lighthouse and seen your LCP score in red there's a good chance the problem is an image.
You optimized your JavaScript. You split your bundles. You added CDN caching. But your hero image is still 2MB and the browser is stuck waiting for it. Your LCP is gone.
And here's the thing most developers don't realize in 70–80% of web apps, the LCP element is an image. Hero banners, product shots, blog covers. That one image decides whether your Core Web Vitals pass or fail.
Bad LCP means lower rankings. Lower rankings means fewer users. Fewer users means your app doesn't grow.
This post is everything I know about fixing that.
What Is LCP and Why Images Break It



Largest Contentful Paint (LCP) measures how long the largest visible element takes to show up on screen.
In 70–80% of web apps, that element is an image hero banner, product image, featured blog image, or something similar.
Google considers LCP good if it's under 2.5 seconds.
But most developers think LCP is just about download speed. It's not.
LCP isn't just download time. It includes TTFB (Time to First Byte), resource discovery, download time, decoding, and rendering. So even if your image is compressed, if it's loaded through JavaScript or blocked behind heavy scripts, LCP will still be bad.
Step 1: Fix LCP Image Priority
If your above-the-fold image has loading="lazy", you already have a problem.
❌ Don't do this
<img src="/hero.jpg" loading="lazy" /><img src="/hero.jpg" loading="lazy" />
Lazy loading delays LCP. It's meant for images below the fold not for your hero image.
✅ Do this instead
<link rel="preload" as="image" href="/hero.jpg" /><img src="/hero.jpg" fetchpriority="high" alt="Hero" /><link rel="preload" as="image" href="/hero.jpg" /><img src="/hero.jpg" fetchpriority="high" alt="Hero" />
This tells the browser to load that image first.
In NextJS, pass the priority prop on the Image component:
<Image src="/hero.jpg" priority alt="Hero" />;<Image src="/hero.jpg" priority alt="Hero" />;
That one change alone can drop LCP by 300–800ms.
Step 2: Use LQIP (Low Quality Image Placeholder)

LQIP = Low Quality Image Placeholder.
Instead of showing a blank space while the image loads, you show a tiny blurred version usually 10–20px wide, or a base64-encoded thumbnail. Then swap it once the full image is ready.
Even if the real image takes 1.8 seconds, the user sees something right away. That changes the feel of the entire page.
Basic HTML version
<imgsrc="data:image/jpeg;base64,/9j/4AAQSk..."data-src="/real-image.jpg"class="blur"/><imgsrc="data:image/jpeg;base64,/9j/4AAQSk..."data-src="/real-image.jpg"class="blur"/>
NextJS version
<Imagesrc="/hero.jpg"placeholder="blur"blurDataURL="data:image/jpeg;base64,..."/>;<Imagesrc="/hero.jpg"placeholder="blur"blurDataURL="data:image/jpeg;base64,..."/>;
When to skip LQIP
- Small icons
- Images under 10kb
- Below-the-fold content
- Already compressed thumbnails
No point adding complexity for images that load instantly anyway.
Step 3: Serve Different Images by Screen Size
Your users are on different devices.
Mobile: 375px. Tablet: 768px. Desktop: 1440px+.
Sending a 2000px image to a mobile phone is just wasted bandwidth. Without
srcset, a phone downloads the same oversized image meant for a 27-inch
monitor. That's bad for performance and bad for users on slow connections.
srcset and sizes
<imgsrc="image-800.jpg"srcset="image-400.jpg 400w, image-800.jpg 800w, image-1600.jpg 1600w"sizes="(max-width: 768px) 100vw, 50vw"alt="Product"/><imgsrc="image-800.jpg"srcset="image-400.jpg 400w, image-800.jpg 800w, image-1600.jpg 1600w"sizes="(max-width: 768px) 100vw, 50vw"alt="Product"/>
The browser picks the smallest image that fits. Less data, faster load, lower LCP.
Step 4: Handle Pixel Density (Retina Screens)
Not all screens render pixels the same way.
devicePixelRatio = 1→ standard screendevicePixelRatio = 2→ Retina / HiDPIdevicePixelRatio = 3→ flagship phones
If you only serve 1x images on a 2x screen, everything looks blurry.
Density descriptors
<img src="logo.jpg" srcset="logo.jpg 1x, logo@2x.jpg 2x" alt="Logo" /><img src="logo.jpg" srcset="logo.jpg 1x, logo@2x.jpg 2x" alt="Logo" />
The browser picks the right version based on the screen.
Don't force 2x images on everyone that doubles file size for users who don't need it. Let the browser decide.
Step 5: Orientation-Based Images
Sometimes the same image doesn't work for both portrait and landscape. The crop changes, the composition breaks.
Use <picture> for this:
<picture><source media="(orientation: portrait)" srcset="portrait.jpg" /><source media="(orientation: landscape)" srcset="landscape.jpg" /><img src="landscape.jpg" alt="Hero" /></picture><picture><source media="(orientation: portrait)" srcset="portrait.jpg" /><source media="(orientation: landscape)" srcset="landscape.jpg" /><img src="landscape.jpg" alt="Hero" /></picture>
Only use orientation switching when the design actually needs different crops. Don't add it everywhere it's extra work for minimal gain in most cases.
Step 6: Use WebP and AVIF
-
JPEG and PNG are old and heavy.
-
WebP and AVIF give you 25–50% smaller files with the same visual quality.
Fallback structure
<picture><source srcset="image.avif" type="image/avif" /><source srcset="image.webp" type="image/webp" /><img src="image.jpg" alt="Product" /></picture><picture><source srcset="image.avif" type="image/avif" /><source srcset="image.webp" type="image/webp" /><img src="image.jpg" alt="Product" /></picture>
The browser picks the best format it supports. If it can handle AVIF, it uses that. Otherwise WebP. Otherwise JPEG.
CDNs like Cloudinary, ImageKit, and Fastly can auto-convert
formats for you. The NextJS Image component does this too.
Step 7: Set Width and Height
If you don't set width and height on your images, the browser doesn't know how much space to reserve. That causes layout shift your CLS score goes up, and the page feels jumpy.
<img src="image.jpg" width="800" height="600" alt="Product" /><img src="image.jpg" width="800" height="600" alt="Product" />
Or use aspect-ratio in CSS. Either way, tell the browser the dimensions upfront.
Step 8: Don't Use CSS Background for LCP Images
Problem
If your hero image looks like this:
.hero {background-image: url("/hero.jpg");}.hero {background-image: url("/hero.jpg");}
The browser might not prioritize it. CSS backgrounds are treated differently they're discovered later in the rendering pipeline.
Use <img> tags for anything that needs to load fast. Save background-image for decorative stuff.
How This Works in Production
Here's what the workflow looks like in a real project:
1. Find the LCP element
Open Chrome DevTools, run Lighthouse, or use PageSpeed Insights. Find out which element is your LCP it's usually an image.
2. Fix loading priority
Remove loading="lazy" from that image. Add <link rel="preload">. Set fetchpriority="high". Make sure no heavy JS blocks it.
3. Generate image variants
Image variant checklist
For your key images, create multiple versions:
- Widths: 400w, 800w, 1200w, 1600w
- Densities: 1x, 2x
- Formats: WebP, AVIF
Automate this with your build pipeline or CDN.
4. Add LQIP only where it matters
Don't blur every image. Apply LQIP to your hero image and a few above-the-fold images. That's it.
5. Watch real user data
Lab scores are just lab scores. Track real LCP, CLS, and FCP through analytics or RUM tools like Vercel Analytics or Google CrUX.
Mistakes I Keep Seeing
- Lazy loading the hero image 2. Loading images after a large JS bundle finishes 3. Sending desktop-size images to all devices 4. No width/height causing layout shift 5. Ignoring pixel density 6. Adding LQIP to every single image 7. Using
background-imagefor the main content image
If your LCP image gets injected via JavaScript after hydration, you're adding delay for no reason.
When to Stop Optimizing
Don't go overboard
- Don't generate 12 variants for a 24px icon
- Don't use orientation switching for simple layouts
- Don't add LQIP for 5kb images
- Don't force AVIF if your users are on browsers that don't support it
The goal is better performance, not more complexity.
Wrapping Up
Image optimization isn't just about compression.
It's about sending the right size, in the right format, at the right time, to the right device.
If your LCP image loads fast, the whole app feels fast. And when the app feels fast, people stay.
Thank you for reading. If you found this useful, share it with someone who needs it.