A partner brought us a marketing site with a 4.2 second Largest Contentful Paint on a mid-range Android phone. The brief was a rebuild. We spent two days measuring instead, and shipped 900ms on the existing codebase.
Measure the thing, not the feeling
The first useful move is finding out which element is actually the LCP candidate. It is very often not the one everybody assumes.
new PerformanceObserver((list) => {
const entry = list.getEntries().at(-1);
console.log(entry.element, entry.startTime);
}).observe({ type: 'largest-contentful-paint', buffered: true });
On this site it was the hero heading, not the hero image — which meant every hour that had gone into image compression had been spent on the wrong thing. The heading was late because it waited on a webfont that waited on a stylesheet that waited on a render-blocking script in the head.
What actually moved the number
- Self-hosted the variable font and preloaded it. Two round trips to a font CDN disappeared. −1.4s.
- Set
font-display: swap. The heading now paints in a fallback face immediately rather than holding the frame. −0.9s. - Deferred the consent script. It was blocking parse in the head to save a flash that nobody had ever complained about. −0.7s.
- Inlined the critical CSS for the header and hero. −0.3s, and the last one worth doing.
What we did not do
We did not rewrite the site, change the framework, or add a caching plugin. None of those would have touched the font chain, and two of them would have added a migration risk to a site that was converting fine.
The general lesson is dull and holds up almost every time: a rebuild is a way of avoiding the measurement, and the measurement is usually cheaper than the rebuild.