Skip to content

core: implement UKM Invalidate fallback for LCP#16956

Merged
TravenReese merged 12 commits intomainfrom
lcp-fallback
Apr 10, 2026
Merged

core: implement UKM Invalidate fallback for LCP#16956
TravenReese merged 12 commits intomainfrom
lcp-fallback

Conversation

@TravenReese
Copy link
Copy Markdown
Collaborator

If no standard LCP candidates are found in the trace, fall back to using the last UKM Invalidate event as a mock candidate. This prevents NO_LCP errors when standard candidates are missing.

@TravenReese TravenReese requested a review from a team as a code owner April 8, 2026 21:26
@TravenReese TravenReese requested review from paulirish and removed request for a team April 8, 2026 21:26
@TravenReese TravenReese changed the title feat(core): implement UKM Invalidate fallback for LCP core: implement UKM Invalidate fallback for LCP Apr 8, 2026
@TravenReese TravenReese requested a review from connorjclark April 8, 2026 21:28
Copy link
Copy Markdown
Member

@paulirish paulirish left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

as discussed.. I don't love the data signals we have to work with.. it appears our repro cases are missing trace events..

eg our repro trace has 12 NavStartToLargestContentfulPaint::Invalidate::AllFrames::UKM events and zero NavStartToLargestContentfulPaint::Candidate::AllFrames::UKM (and zero NavStartToLargestContentfulPaint::AllFrames::UMA)

and that just doesnt make sense.

but.. as it has no 'largestContentfulPaint::Candidate' (or regular invalidate).. and yet we know it paints and looks good. so yeah very weird.

but yeah this solution is good for now, until we're ready to dig into the lossy trace event emission problems. :)

pid: lastInvalidate.pid,
tid: lastInvalidate.tid,
args: {
frame: 'main_frame', // Mocked frame ID
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

you can compute this if you want. (right now it seems like you dont have to .. but.. i wouldnt mind trying to set this correctly)

Comment on lines +632 to +650
// If no standard LCP candidate is found, try the UKM AllFramesEvents.
if (!maxLcpAcrossFrames) {
const ukmEvents = events.filter(
(e) =>
e.name.includes('LargestContentfulPaint') && e.name.includes('UKM')
);

// In the rare cases this whole fallback is necessary, the
// NavStartToLargestContentfulPaint::Candidate::AllFrames::UKM events are missing too.
// As a result, the only useful signal left is the AllFrames invalidates.
// Not ideal since they are 1 paint behind, but.. better than the dreaded
// NO_LCP error
const targetEventName =
'NavStartToLargestContentfulPaint::Invalidate::AllFrames::UKM';
const ukmInvalidates = ukmEvents.filter((e) => e.name === targetEventName);

if (ukmInvalidates.length > 0) {
ukmInvalidates.sort((a, b) => a.ts - b.ts);
const lastInvalidate = ukmInvalidates[ukmInvalidates.length - 1];
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
// If no standard LCP candidate is found, try the UKM AllFramesEvents.
if (!maxLcpAcrossFrames) {
const ukmEvents = events.filter(
(e) =>
e.name.includes('LargestContentfulPaint') && e.name.includes('UKM')
);
// In the rare cases this whole fallback is necessary, the
// NavStartToLargestContentfulPaint::Candidate::AllFrames::UKM events are missing too.
// As a result, the only useful signal left is the AllFrames invalidates.
// Not ideal since they are 1 paint behind, but.. better than the dreaded
// NO_LCP error
const targetEventName =
'NavStartToLargestContentfulPaint::Invalidate::AllFrames::UKM';
const ukmInvalidates = ukmEvents.filter((e) => e.name === targetEventName);
if (ukmInvalidates.length > 0) {
ukmInvalidates.sort((a, b) => a.ts - b.ts);
const lastInvalidate = ukmInvalidates[ukmInvalidates.length - 1];
// If no standard LCP candidate is found, try the UKM AllFramesEvents.
if (!maxLcpAcrossFrames) {
// In the rare cases this whole fallback is necessary, the
// NavStartToLargestContentfulPaint::Candidate::AllFrames::UKM events are missing too.
// As a result, the only useful signal left is the AllFrames invalidates.
// Not ideal since they are 1 paint behind, but.. better than the dreaded
// NO_LCP error
const targetEventName =
'NavStartToLargestContentfulPaint::Invalidate::AllFrames::UKM';
const ukmInvalidates = events.filter(e => e.name === targetEventName);
if (ukmInvalidates.length > 0) {
const lastInvalidate = .sort((a, b) => a.ts - b.ts).at(-1);

fwiw this isnt just for code golfing sake :)

the
events.filter(e => e.name.includes(x) && e.name.includes(y)) is pretty expensive (traces often have 5M events+ ) and.. we really only care about this singular name, right?

( also fwiw you can just hit 'commit' here and then pull locally. )

@TravenReese TravenReese merged commit 72ac057 into main Apr 10, 2026
31 checks passed
@TravenReese TravenReese deleted the lcp-fallback branch April 10, 2026 14:33
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants