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.
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.
A minimal product-detail page. Hovering the product photo inserts a larger zoom image, exactly like typical retailer zoom widgets.
Entries from PerformanceObserver({type: 'largest-contentful-paint'}),
recorded live in this tab:
| # | startTime | size | element | cause |
|---|---|---|---|---|
| waiting for entries... | ||||
Hover the product image to see whether this browser reports a hover-caused LCP entry.
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.
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).
A non-component release build of Chromium 153.0.7981.0 (amd64) with the
prototype included, packaged as a .deb:
# verify + install (Debian / Ubuntu)
sha256sum -c SHA256SUMS
sudo dpkg -i chromium-lcp-mouseover-153.0.7981.0-amd64.deb
# sha256
ba2979f4336bf0d489420b370d73433ab8305b0770446ebd486f20797924efde
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.
# 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