Back to Blog
Performance
Mobile LCP: cutting hero image cost on a CMS-driven Next.js site
February 15, 20265 min read
PerformanceCore Web VitalsNext.jsImages
The setup
The site is a Next.js + GraphQL build for a high-traffic UK consumer brand on a headless CMS. Landing pages share a common pattern: a hero banner module followed by content carousels, feature tables, and module-driven sections. Images are served through a CDN that supports query-string transformations (
?width=…&quality=…).Mobile LCP on the landing pages was visibly poor in field data — the hero image consistently missed the "good" Core Web Vitals threshold for a meaningful share of traffic.
What was actually slow
The page was rendered server-side, so the HTML arrived quickly. The bottleneck was the hero image:
srcset was generous but unfocused.** The image had srcset entries from 320w up to 2400w, but sizes was a single 100vw everywhere — meaning mobile devices sometimes still requested oversized variants.What changed
Three small changes, in priority order:
1. **
fetchpriority="high" on the LCP candidate.** Only the first hero image on the page got it — every other module fell back to default. Done at the renderer level so editors couldn't accidentally promote a non-LCP image.2. **Quality tuned per breakpoint.** The CDN URL builder now emits
quality=70 for mobile widths and quality=82 for desktop. Visually it's hard to tell on a phone; the byte savings on the hero alone were significant.3. **Tighter
sizes.** Replaced the blanket 100vw with a media-query-driven sizes attribute that reflected the actual layout ((max-width: 768px) 100vw, 720px for the hero column). The browser stopped requesting oversized variants on mid-size phones.I avoided the obvious "preload" approach. A
from the document head would force the browser to fetch the hero before it knew which variant was needed, which on a CMS-driven layout meant either preloading the wrong image (when the editor swapped the module) or duplicating the request.Trade-offs worth flagging
quality server-side meant editors lost the ability to bump quality back up for branded campaign images. We added a CMS toggle for that, gated to a small allow-list.fetchpriority is a hint, not a guarantee.** On low-end Android devices behind a memory-constrained Chrome, the browser still occasionally deprioritized the image when many ads/3rd-party scripts were in flight. The right long-term fix is reducing 3rd-party script weight, not chasing the priority hint harder.sizes plus aspect-ratio meant we had to enforce CMS aspect ratios — otherwise an editor uploading a 1:1 image into a 16:9 slot caused CLS regressions. We added validation at the CMS level.What actually moved
LCP on model pages improved on mobile field data within the next two collection windows. The single biggest contribution was the quality change — image transfer size dropped, and that dominates LCP on slow connections.
fetchpriority helped at the margin. The sizes fix mostly removed the worst-case spikes on mid-range phones.I'd rank the changes the same way next time: **bytes first, priority second, layout fixes third.**