Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Save INP target after interactions to reduce null values when removed from the DOM #477

Merged
merged 11 commits into from
May 10, 2024
38 changes: 34 additions & 4 deletions src/attribution/onINP.ts
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,9 @@ const entryToRenderTimeMap: WeakMap<
DOMHighResTimeStamp
> = new WeakMap();

// A mapping of interactions to the target selector
export const interactionTargetMap: Map<number, string> = new Map();

// A reference to the idle task used to clean up entries from the above
// variables. If the value is -1 it means no task is queue, and if it's
// greater than -1 the value corresponds to the idle callback handle.
Expand All @@ -77,6 +80,22 @@ const handleLoAFEntries = (entries: PerformanceLongAnimationFrameTiming[]) => {
entries.forEach((entry) => pendingLoAFs.push(entry));
};

// Save the selector early in case not available later if removed from DOM
const saveInteractionSelector = (entry: PerformanceEventTiming) => {
const interactionId = entry.interactionId;
if (!interactionId) return;

// Don't run the getSelector for keyboard events after first one as there could
// be a lot of them in short fashion when typing.
if (entry.entryType !== 'first-input' && entry.name.startsWith('key')) return;
philipwalton marked this conversation as resolved.
Show resolved Hide resolved

// Save any new selectors
if (!interactionTargetMap.get(interactionId) && entry.target) {
const selector = getSelector(entry.target);
if (selector) interactionTargetMap.set(interactionId, selector);
}
};

/**
* Groups entries that were presented within the same animation frame by
* a common `renderTime`. This function works by referencing
Expand Down Expand Up @@ -141,6 +160,12 @@ const cleanupEntries = () => {
// more than sufficient.
previousRenderTimes = previousRenderTimes.slice(-50);

// We only need the 10 last targets;
if (interactionTargetMap.size > 10) {
const keys = Array.from(interactionTargetMap.keys()).slice(0, -10);
keys.forEach((k) => interactionTargetMap.delete(k));
}

// Keep all render times that are part of a pending INP candidate or
// that occurred within the 50 most recently-dispatched animation frames.
const renderTimesToKeep = new Set(
Expand Down Expand Up @@ -169,7 +194,10 @@ const cleanupEntries = () => {
idleHandle = -1;
};

entryPreProcessingCallbacks.push(groupEntriesByRenderTime);
entryPreProcessingCallbacks.push(
saveInteractionSelector,
groupEntriesByRenderTime,
);

const getIntersectingLoAFs = (
start: DOMHighResTimeStamp,
Expand Down Expand Up @@ -211,7 +239,11 @@ const attributeINP = (metric: INPMetric): INPMetricWithAttribution => {
// first one found in the entry list.
// TODO: when the following bug is fixed just use `firstInteractionEntry`.
// https://bugs.chromium.org/p/chromium/issues/detail?id=1367329
// We also fallback to interactionTargetMap for when target removed from DOM
const firstEntryWithTarget = metric.entries.find((entry) => entry.target);
const interactionTarget = firstEntryWithTarget
? getSelector(firstEntryWithTarget.target)
: interactionTargetMap.get(firstEntry.interactionId) || '';

// Since entry durations are rounded to the nearest 8ms, we need to clamp
// the `nextPaintTime` value to be higher than the `processingEnd` or
Expand All @@ -226,9 +258,7 @@ const attributeINP = (metric: INPMetric): INPMetricWithAttribution => {
const nextPaintTime = Math.max.apply(Math, nextPaintTimeCandidates);

const attribution: INPAttribution = {
interactionTarget: getSelector(
firstEntryWithTarget && firstEntryWithTarget.target,
),
interactionTarget: interactionTarget,
interactionType: firstEntry.name.startsWith('key') ? 'keyboard' : 'pointer',
interactionTime: firstEntry.startTime,
nextPaintTime: nextPaintTime,
Expand Down
Loading