When Your "Fixed" CLS Problem Isn't Fixed Yet

Fairprice, one of Singapore's largest online grocery stores, tracks user-centric performance metrics closely. Among FCP, LCP, FID, and CLS, the Cumulative Layout Shift metric proved the most elusive to pin down. The product detail page had more than 0.1 CLS value for numerous URLs, according to the Google Search Console Core Web Vitals Report. That was the first signal that something was off, but synthetic testing tools initially couldn't reproduce the issue.

The Synthetic Tools Blinked First

We began debugging with standard synthetic tools. A Lighthouse run reported a CLS of .004 — comfortably within the "good" range. Its diagnostic section also failed to flag any single element causing a high shift score. WebPageTest's filmstrip view didn't highlight any contributors to the perceived high CLS either.

That left us with a contradiction: real-user data indicated a CLS problem, yet synthetic tests showed a clean bill of health. The key insight here is that CLS records individual layout shift scores during the entire lifespan of the page and adds them. Lighthouse and WebPageTest primarily capture what happens during and shortly after page load. If the shift happens later — say, after a user interaction — synthetic tools will miss it.

We turned to the Web Vitals Chrome extension, which records CLS while a user is actively interacting with the page. Reproducing various user actions, we found the CLS score increased specifically when the user employed the image magnify feature — hovering over the main product image to see a zoomed view.

To confirm the culprit, we used the layout shift detection snippet from web.dev which logs a console message whenever a shift occurs. During the mouseover on the product image, the console output confirmed our suspicion. The same class of issue had hit ASDA, which raised a Chromium bug report for it.

Root Cause: The Magnifying Lens

The product detail page's image magnify feature relies on the react-image zoom library (built on js-image-zoom). These libraries render a "lens" — a square that moves with the mouse cursor across the product image. The lens's top and left positions were being updated on every mouse movement. The browser interpreted those position changes as a layout shift, thus inflating the CLS score.

We checked other popular React magnifier libraries (react-image-magnify, react-image-magnifiers) and found they all exhibit the same problem — this is a systemic issue with how these components are typically built.

The Fix: Steer Movement Onto Its Own Layer

The solution was to stop changing the lens element's logical position in the document flow. Instead of updating left and top, we modified the underlying js-image zoom library to use transform translate. Moving an element with transform places it on its own compositor layer; the layout of the page remains stable, so no layout shift is recorded.

We also filed a pull request against the original repository so other developers can benefit from the fix. As a bonus, the compositor-layer movement made the magnify interaction noticeably smoother for users than the old top-left positioning ever was.

After deploying this change, the number of pages impacted by CLS on the product detail page dropped by 98%.

Other CLS Fixes Across The Site

The magnify issue was the most specific, but not the only source of layout shift. We tackled several other recurring contributors:

  • Web fonts: Late-loading fonts cause content to flash and shift as they render. We reduced that impact by self-hosting fonts (instead of third-party CDNs), preloading the font files, and setting font-display: optional.
  • Images: If an <img> lacks explicit width and height, content below it jumps when the image arrives. On our Next.js site, we moved to the built-in next/images component, which handles intrinsic aspect-ratio reservations as part of its implementation.
  • Infinite scroll: On product listing pages, users scrolling to the bottom briefly saw the footer before the next batch of API data loaded. We now trigger the data fetch before the user absolutely reaches the bottom, and reserve space showing skeleton placeholders during fetch. The footer no longer flashes mid-load.

What To Reach For During A CLS Hunt

Debugging CLS benefits from choosing tools based on when the shift is happening:

  • Lighthouse / WebPageTest: Great for shifts happening during the initial page load. They will miss anything triggered by post-load user interactions.
  • Web Vitals extension: Essential when real-user data shows high CLS but synthetic tools report a low score. It captures interaction-based shifts that occur after load and can help you pinpoint the exact trigger.
  • Google Search Console: Since it's based on real-world field data, it catches issues that occur anytime during a page's lifecycle. Recheck this report after deploying a fix; updates appear within days.

Measuring CLS is still evolving, so tracking its latest definition will help stay ahead of future changes. Fixing layout shift patterns — especially by avoiding style mutations that affects document flow — remains the durable strategy for keeping this metric in check.