Desktop LCP: mouse-over image zoom triggers a new LCP entry

#40211421 Filed 2022-01-17 Accepted · P3 / S5 Blink > PerformanceAPIs

The problem

E-commerce product pages commonly show a zoomed image when the user hovers the product photo. Hover events (mouseover, pointerover, ...) do not stop LCP observation -- only clicks, key presses and taps do. So when the hover handler inserts a larger image, that image becomes a new, late LCP candidate and inflates the page's reported LCP, even though the paint was caused by the user, not by slow loading.

Before you try the demo

LCP observation stops at the first click, tap or key press. Do not click or type after loading this page -- just move the mouse over the product image below. Reload the page to start over.

Live demo

A minimal product-detail page. Hovering the product photo inserts a larger zoom image, exactly like typical retailer zoom widgets.

zoom appears here on hover
The small image (260×260) is the legitimate LCP of this page. The hover-inserted zoom image renders at up to 420×420 and is therefore reported as a new, larger LCP candidate.

Observed LCP entries

Entries from PerformanceObserver({type: 'largest-contentful-paint'}), recorded live in this tab:

#startTimesizeelementcause
waiting for entries...

Verdict

Hover the product image to see whether this browser reports a hover-caused LCP entry.

Root cause

LCP terminates on interactions that get an interaction ID (click, tap, key press). Hover events never receive one, and the DOM mutations plus paints they cause carry no attribution linking them back to the hover. The paint timing code therefore cannot distinguish "revealed by the user's mouse" from "finally finished loading".

An earlier heuristic (a ~500ms time window after mouseover over the LCP element, CLs 3395315 / 4061623) was unreliable for CSS-visibility-based widgets and delayed work, and was removed again in CL 5789151 with the recommendation to solve it via JavaScript task attribution.

The fix (prototype)

Behind the default-off runtime feature LCPMouseoverHeuristics, Blink now reuses the soft-navigation / InteractionContentfulPaint task attribution infrastructure for hover events:

• all hover events share one lazily-created hover context
• a task scope is only created when a hover listener is actually registered along the dispatch path (hover events are frequent on desktop; the common listener-less case stays cheap)
• elements whose style is affected by :hover rules get their subtree attributed to the hover context, covering pure-CSS zoom widgets with no JS at all
• image and text paints attributed to the hover context are excluded from the hard-navigation LCP candidate set -- everything else keeps reporting normally

Covered by red/green unit tests in image_paint_timing_detector_test.cc and text_paint_timing_detector_test.cc (inserted image, src-swapped image, CSS :hover-revealed image, inserted text, listener gating, and a no-over-exclusion guard).

Download: custom build (Linux x64)

A non-component release build of Chromium 153.0.7981.0 (amd64) with the prototype included, packaged as a .deb:

⬇ chromium-lcp-mouseover-153.0.7981.0-amd64.deb (563 MB) SHA256SUMS
# verify + install (Debian / Ubuntu)
sha256sum -c SHA256SUMS
sudo dpkg -i chromium-lcp-mouseover-153.0.7981.0-amd64.deb

# sha256
ba2979f4336bf0d489420b370d73433ab8305b0770446ebd486f20797924efde

Note

The fix is compiled in but off by default -- it only activates with --enable-blink-features=LCPMouseoverHeuristics. This makes A/B comparison possible with a single binary.

How to test

# A: current behavior (bug reproduces)
chromium-browser-stable

# B: prototype enabled (hover-caused paints excluded from LCP)
chromium-browser-stable --enable-blink-features=LCPMouseoverHeuristics

# then load this page, hover the product image and compare:
#   A: a second, larger LCP entry appears after hover (red "after hover" row)
#   B: LCP stays at the original product image