Image optimization that moves the needle is about more than just compression. Learn how to use modern formats and responsive delivery to slash LCP times.

I spent three days last month staring at a Lighthouse report that refused to budge. The Largest Contentful Paint (LCP) on our landing page was hovering around 3.2 seconds, and no amount of minifying CSS or deferring JavaScript seemed to help. It turned out the culprit was a "hero" image—a beautiful, 2.4MB PNG that was being served at full desktop resolution to every single visitor, regardless of their device.
If you're chasing better performance, image optimization that moves the needle isn't about running every asset through a lossy compressor. It’s about being surgical with how you deliver those bytes.
When we talk about performance, we’re usually talking about the user’s experience of waiting. If your main content is an image, that image is the gatekeeper of your LCP score. I see developers spend hours on database tuning—like indexing strategy for app developers: stop slow queries—only to let a massive, unoptimized hero banner destroy their frontend metrics.
We first tried simply swapping the PNG for a WebP version. It shaved off about 400ms, which was nice, but it didn't solve the core issue: the browser was still downloading a 1920px image for a mobile screen. We were wasting bandwidth and time.
To actually make a difference, we had to move toward a responsive delivery strategy.
We stopped using standard <img> tags for our hero sections and moved to the <picture> element. This allows us to serve different source files based on the viewport width.
HTMLstyle="color:#808080"><style="color:#4EC9B0">picture> style="color:#808080"><style="color:#4EC9B0">source srcset="hero-mobile.webp" media="(max-width: 768px)" type="image/webp"> style="color:#808080"><style="color:#4EC9B0">source srcset="hero-desktop.webp" media="(min-width: 769px)" type="image/webp"> style="color:#808080"><style="color:#4EC9B0">img src="hero-desktop.jpg" alt="Hero banner" width="1200" height="600" fetchpriority="high"> style="color:#808080"></style="color:#4EC9B0">picture>
By adding fetchpriority="high", we explicitly tell the browser to prioritize this image during the initial paint. This is a small tweak that, in our testing, improved LCP by roughly 600ms on throttled 4G connections.

It's tempting to automate everything, but I've learned the hard way that one size doesn't fit all. We tried a blanket automated conversion script, but it ended up mangling some of our high-contrast UI icons.
Now, we follow a tiered approach:
sharp in Node.js.If you're struggling with LCP, I highly recommend checking out Fixing LCP: The Usual Suspects and Real-World Fixes to see if your image pipeline is the bottleneck. Sometimes, the issue isn't the image size at all—it's the server response time.
One thing I see often is developers ignoring the impact of their CDN configuration. Even if your images are optimized, if they aren't being cached correctly at the edge, you're paying a latency penalty every time.
We configured our Cache-Control headers to be aggressive:
Cache-Control: public, max-age=31536000, immutable
This ensures that once a user visits, the image is served from their disk cache for a year. If we update the image, we use a versioned filename (e.g., hero-v2.webp). It’s a simple pattern, but it prevents unnecessary re-validation requests.
Is AVIF always better than WebP? Generally, yes, for file size. However, the encoding time can be significantly higher. If you're generating these on-the-fly, watch your server CPU usage.
Should I lazy load my hero image? Never. Lazy loading the LCP element is a great way to tank your metrics. Always ensure your primary hero image is excluded from lazy loading.
How do I decide which sizes to generate? Don't guess. Look at your analytics to see the most common screen resolutions your users visit from, and generate 3-4 variants that cover 90% of those cases.

I’m still not entirely satisfied with our current image pipeline. We're currently looking into "client hints," which would allow the browser to tell the server exactly what it needs before the request is even fulfilled. It’s complex, and I'm worried about browser support inconsistencies, but it feels like the next logical step for true performance optimization.
Performance isn't a one-and-done project. It's a continuous cycle of measuring, breaking things, and refining your approach. If you're looking for other ways to optimize, ensure you aren't ignoring your backend, as eliminating N+1 queries in Eloquent: a pragmatic approach can often be the difference between a fast page and a frustrating one. Just start with the low-hanging fruit—the images—and go from there.
Cutting JavaScript bundle size doesn't require a rewrite. Learn how to audit your dependencies, tree-shake dead code, and optimize your build for speed.