This engagement is covered by a confidentiality obligation, so what follows is the mechanism with the client removed. The mechanism is the part worth copying anyway — the bug was in what the browser was timing, not in how heavy the page was.
Largest Contentful Paint on product pages moved between 1.0 and 6.4 seconds from one load to the next. Depending on the session, the same template on the same code landed in all three of Google's bands:
good needs improvement poor
The mean was never the problem. The variance was — and the 75th percentile Google assesses is drawn from the slow end of that spread, not the middle of it.
How this was seen. Field data from real sessions, read at the 75th percentile. A single synthetic Lighthouse load cannot report a distribution; it reports one load on a clean machine. The field data showed the variance. The lab data did not, which is why the page had looked healthy every time anyone checked it by hand.
Product pages were rendered as Liquid server-side HTML, and the React app replaced that HTML when it mounted. ReactDOM.render() was throwing away the server-rendered hero image and re-mounting it. Largest Contentful Paint was therefore being measured from the remount, not from the original server-rendered paint.
This is why the usual levers did nothing. Compressing the image, resizing it, changing its format, tuning the CDN — none of it could move a measurement that was not timing the server-rendered image in the first place.
The <head> carried around forty blocking scripts, which made React's boot time non-deterministic. The remount therefore landed at a different moment on every load, and that is where the run-to-run spread came from. The remount is what broke the measurement; the blocking scripts are what made it move around.
Two changes, in that order. First, make the server-rendered placeholder paint at First Contentful Paint without requiring JavaScript: CSS had been hiding it, so it never painted before React booted and the server-rendered HTML was doing no work for anybody. Second, stop the client from destroying it on mount, so the paint the browser has already made is the paint that gets measured.
Neither change touched image weight, the CDN, or the third-party scripts. Those scripts are still a real cost and still worth reducing. They were not the reason the number was wrong.
Field Largest Contentful Paint became consistently 1.1 seconds. Not a better average — a stable number, which is the thing the 75th percentile actually rewards.
On any storefront where a client-side framework hydrates over server-rendered HTML, check whether the framework replaces the LCP element rather than adopting it. That one question is the transferable part of this engagement, and it applies well beyond React and well beyond Liquid.
Three signs you are looking at this bug:
A Lighthouse score cannot show you this, because a score is one load and the problem is a distribution. Image optimisation cannot fix it, because the image is not what is being timed.
Limits. One engagement, one codebase. The mechanism generalises; the magnitude does not. How much time this returns on a given storefront depends on what its LCP element is, how much blocking script sits in front of hydration, and whether its framework is discarding that element at all — which is a question to answer with field data, not with an assumption.
Then it is worth checking which element the browser is actually timing. If your field LCP is worse than your Lighthouse score and the number moves around between runs, this is the first thing I would look at.