Performance isn't a one-time achievement. It's a moving target. On a large e-commerce platform, where new features ship constantly and traffic patterns shift week to week, even a well-optimized site can quietly drift out of shape. This is the story of how our engineering team identified a regression in one of our most important performance metrics, diagnosed the root cause, and methodically clawed our way back to a healthy state.
If you run or build for an e-commerce website, the lessons here should feel familiar, and hopefully useful.
The metric that matters: LCP
Largest Contentful Paint (LCP) measures how long it takes for the largest visible element on a page, usually the main image or a large block of text, to render. It's one of Google's Core Web Vitals, and it's a strong proxy for how fast a page feels to a real user.
The benchmark is clear: to deliver a good experience, aim for an LCP of 2.5 seconds or less on at least 75% of page visits. When a page crosses that threshold reliably, it counts as a “Good URL.” The percentage of Good URLs across a site is a headline indicator of overall performance health, and it directly influences search ranking and conversion.
So when our Good URLs started dropping, we paid attention.
How the regression crept in
Earlier in the year we had done a focused round of LCP optimization and seen real gains in both LCP and Good URLs. We kept monitoring and the numbers held steady for months.
But performance regressions are rarely the result of a single dramatic mistake. Over the following two to three months, we shipped a steady stream of changes across both the client and the server to support new features and requirements. None of them individually broke anything. Collectively, though, LCP began a slow upward creep. For a while it stayed below the level that would impact Good URLs.
Then a major new pricing feature landed, carrying a significant amount of change on our Product Detail Pages (PDPs). That was the tipping point. Shortly after release, the LCP increase grew large enough to start dragging Good URLs down. The impact showed up in our metrics within about a week of the release.
There was also a contributing factor on the monitoring side. While swapping out one application performance monitoring (APM) tool for another, we discovered the new tool was itself degrading LCP. Once we identified it and removed it, the numbers recovered to their previous baseline. It was a useful reminder that even your observability tooling can become a performance liability.
Why LCP is hard to fix
When you find a Core Web Vital problem, the instinct is to fix it immediately and watch the numbers snap back. With LCP, that almost never happens, and it's worth being honest about why:
- Many factors contribute to LCP.It's rarely one thing.
- The problems are not obvious or straightforward. The cause is often buried several layers down.
- A fix that worked before may not work again. The same optimization can deliver a different impact, or none at all, the second time around.
- It demands heavy debugging, research, and trial-and-error. There's no shortcut past the investigation.
- Measurement is slow. You often have to deploy to production and wait several days before the real-world impact is visible.
Given all of this, we didn't try to solve everything at once. Instead we compiled a backlog of concrete line items and observations, each with a plausible positive impact, and committed to executing them iteratively. Smaller, faster releases meant quicker observation cycles, which in turn helped us predict outcomes and refine the plan as we went.
The iterative rollout
What follows is the sequence of optimization releases, roughly in the order they shipped. Each one was deployed, measured in production, and used to inform the next.
v0 — Quick wins on PDPs
The first release targeted both our prescription medicine and over-the-counter (OTC) product detail pages with a set of low-risk improvements: moving SVGs to a CDN after the second fold, removing the preconnect to Google Fonts, moving meta tags to the top of the document head, and minifying CSS for PDPs.
The medicine PDP score moved from 60 to 62, and the OTC PDP score from 50 to 53. Modest, but a start.
v1 — Deeper PDP surgery
Next we focused on the medicine PDP with a more substantial set of changes. We swapped the LCP image from a plain img tag to an optimized image component, tightened up component imports to pull in only what was needed, removed unused CSS, and replaced utility-library functions with leaner in-house equivalents.
We also dynamically imported a login library, added Webpack optimizations including dead-code-aware usedExports, and reworked the product image carousel: inlining conditional logic for share icons, dynamically importing the image gallery, removing an unnecessary higher-order component, and passing device context as a simple prop instead. A one-second timeout was added to a recommendations API so it could never block rendering indefinitely.
The result was a medicine PDP score above 65, and Good URLs began climbing. We deliberately limited these changes to the medicine PDP first, planning to extend them to OTC once the impact was confirmed.
v2 — Extending to OTC
With the medicine PDP improvements validated, we applied the same playbook to the OTC PDP. Good URLs increased from roughly 139K to 153K.
v3 — Marketing tag cleanup
Third-party tags accumulate quietly and cost real performance. We audited and cleaned up our remarketing tags, removing unused tags, triggers, and variables, and pausing a large set of marketing pixels and tags that were no longer needed.
This delivered a two-point score increase at the 50th percentile, and Good URLs rose from 153K to 160K.
v4 — Upgrading the framework
We upgraded our application framework to a newer major version. This was less about an immediate score bump and more about setting a stronger foundation: a much faster minifier (up to 3x faster builds), in-house A/B testing middleware support, a newer rendering runtime, and improved image handling.
The payoff was significant. The 50th-percentile score stayed stable while the 90th percentile improved. Good URLs jumped from 160K to 201K, and the count of “Needs Improvement” URLs collapsed from 35.4K to 2.27K. Framework upgrades can be daunting, but this one paid for itself.
v5 — Tackling layout shift
We shipped Cumulative Layout Shift (CLS) fixes for the desktop medicine and OTC PDPs. LCP gets a lot of attention, but a janky, shifting layout undermines the experience just as much, and it's a Core Web Vital in its own right.
v6 — Smarter edge caching
We changed how we keyed our CDN cache for PDPs. Instead of including the full location code in the cache key, we used only the first two digits (representing the broader region). This dramatically increased the number of cacheable variants that could be shared across users.
The motivation was a stark gap in cache efficiency: our cache hit ratio for bots sat around 75%, but for real organic traffic it was only around 15%. The goal was to lift organic traffic toward 70%.
v7 — Another framework and runtime upgrade
We continued investing in the foundation with another framework major-version upgrade and a runtime upgrade. Scores at both the 50th and 90th percentiles improved by one to three points.
v8 — Trimming a heavy dependency
We removed a widely-used but heavy utility library from the PDPs. The Total Blocking Time (TBT) improved by 0.1 seconds. Small on paper, but these reductions compound.
v9 — Hydration and dead code (planned)
The next phase, still on the roadmap, focuses on framework rehydration optimizations, HTML size reduction, further Webpack tuning, and dead code elimination. The expected payoff is improvement in TBT, FCP, and LCP, driven by avoiding a full client-side re-render and instead updating data cleanly during rehydration.
The backlog: where the real work lives
Beyond the shipped releases, we maintained a prioritized backlog of larger initiatives, each estimated for effort and expected impact. A few themes are worth highlighting because they generalize well to any large web application.
Critical CSS, done properly. Cleaning up a bloated document head, memoizing CSS files, making critical CSS the default for all pages, and removing redundant style injection. Estimated impact: roughly 5% on FCP, 5% on LCP, and 2% on TBT.
Eliminating render-blocking APIs. Our medicine PDP was waiting on a long list of APIs before rendering: dynamic and static product data, descriptions, substitutes, articles, multiple recommendation widgets, cashback, and offers. The key insight was that the static API alone holds the LCP image and enough basic description to stream the first fold. By splitting the PDP into server and client components and wrapping the product carousel in a suspense boundary, we could start streaming the moment the static response arrived, instead of blocking on every API. This was estimated at up to 30% improvement on both FCP and LCP, with diminishing returns at the top end.
Critical render path optimization. Moving context providers and layouts to a per-route basis rather than wrapping the entire application, so each route only loads what it needs.
Removing small libraries and reducing third-party weight. A long tail of single-purpose dependencies, date formatting, pluralization, clipboard helpers, tooltips, infinite scroll, each adds to the bundle. Replacing them with small in-house utilities avoids third-party dependencies and shrinks the bundle.
Replacing the carousel and UI component library. Swapping a third-party carousel library for an in-house implementation, and progressively removing a large UI component library in favor of purpose-built components (radio buttons, toasts, drawers, select boxes). These are big, multi-week efforts, but they meaningfully cut the JavaScript and HTML footprint.
Webpack configuration revisit.Ensuring tree-shaking actually happens, minification is effective, and the build runs in true production-optimized mode. It's surprisingly easy for a build config to drift into a state where it isn't doing what you assume.
Lessons worth keeping
A few principles emerged from this effort that outlast any single fix:
Performance regresses silently. No single feature broke our LCP. A hundred small, reasonable changes did, collectively. Continuous monitoring is the only way to catch the drift before it crosses a threshold that hurts users.
Your tooling can be the problem. An APM tool meant to help us observe performance was actively degrading it. Audit everything that runs in production, including the things you installed to watch production.
Iterate, measure, repeat.Because LCP fixes can't be validated instantly, small and frequent releases beat big-bang rewrites. Each release teaches you something that sharpens the next.
Foundation work pays compounding dividends.The framework and runtime upgrades didn't always produce the flashiest immediate numbers, but they unlocked faster builds, better defaults, and the architectural primitives (streaming, suspense, partial hydration) that made the high-impact fixes possible.
Know your metrics' shape. LCP and FCP have diminishing returns, the closer you get to the threshold, the harder each additional millisecond is to win. TBT, by contrast, has increasing returns. Knowing this lets you spend effort where it actually moves the needle.
Performance optimization on a busy e-commerce site is never finished. But with disciplined measurement, an honest acknowledgment of how hard the problem is, and a willingness to grind through it iteratively, a regression that once felt alarming becomes just another solvable engineering problem.