Skip to content

Commit f720687

Browse files
committed
Merge branch 'v5' into upgrading-v5-doc
2 parents 2fdf8a5 + 3b9acdb commit f720687

File tree

8 files changed

+331
-47
lines changed

8 files changed

+331
-47
lines changed

README.md

Lines changed: 71 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -897,14 +897,6 @@ interface INPAttribution {
897897
* within the frame, only the first time is reported).
898898
*/
899899
interactionTime: DOMHighResTimeStamp;
900-
/**
901-
* The best-guess timestamp of the next paint after the interaction.
902-
* In general, this timestamp is the same as the `startTime + duration` of
903-
* the event timing entry. However, since duration values are rounded to the
904-
* nearest 8ms (and can be rounded down), this value is clamped to always be
905-
* reported after the processing times.
906-
*/
907-
nextPaintTime: DOMHighResTimeStamp;
908900
/**
909901
* The type of interaction, based on the event type of the `event` entry
910902
* that corresponds to the interaction (i.e. the first `event` entry
@@ -913,20 +905,19 @@ interface INPAttribution {
913905
* and for "keydown" or "keyup" events this will be "keyboard".
914906
*/
915907
interactionType: 'pointer' | 'keyboard';
908+
/**
909+
* The best-guess timestamp of the next paint after the interaction.
910+
* In general, this timestamp is the same as the `startTime + duration` of
911+
* the event timing entry. However, since duration values are rounded to the
912+
* nearest 8ms (and can be rounded down), this value is clamped to always be
913+
* reported after the processing times.
914+
*/
915+
nextPaintTime: DOMHighResTimeStamp;
916916
/**
917917
* An array of Event Timing entries that were processed within the same
918918
* animation frame as the INP candidate interaction.
919919
*/
920920
processedEventEntries: PerformanceEventTiming[];
921-
/**
922-
* If the browser supports the Long Animation Frame API, this array will
923-
* include any `long-animation-frame` entries that intersect with the INP
924-
* candidate interaction's `startTime` and the `processingEnd` time of the
925-
* last event processed within that animation frame. If the browser does not
926-
* support the Long Animation Frame API or no `long-animation-frame` entries
927-
* are detect, this array will be empty.
928-
*/
929-
longAnimationFrameEntries: PerformanceLongAnimationFrameTiming[];
930921
/**
931922
* The time from when the user interacted with the page until when the
932923
* browser was first able to start processing event listeners for that
@@ -955,6 +946,68 @@ interface INPAttribution {
955946
* (e.g. usually in the `dom-interactive` phase) it can result in long delays.
956947
*/
957948
loadState: LoadState;
949+
/**
950+
* If the browser supports the Long Animation Frame API, this array will
951+
* include any `long-animation-frame` entries that intersect with the INP
952+
* candidate interaction's `startTime` and the `processingEnd` time of the
953+
* last event processed within that animation frame. If the browser does not
954+
* support the Long Animation Frame API or no `long-animation-frame` entries
955+
* are detected, this array will be empty.
956+
*/
957+
longAnimationFrameEntries: PerformanceLongAnimationFrameTiming[];
958+
/**
959+
* Summary information about the longest script entry intersecting the INP
960+
* duration. Note, only script entries above 5 milliseconds are reported by
961+
* the Long Animation Frame API.
962+
*/
963+
longestScript?: INPLongestScriptSummary;
964+
/**
965+
* The total duration of Long Animation Frame scripts that intersect the INP
966+
* duration excluding any forced style and layout (that is included in
967+
* totalStyleAndLayout). Note, this is limited to scripts > 5 milliseconds.
968+
*/
969+
totalScriptDuration?: number;
970+
/**
971+
* The total style and layout duration from any Long Animation Frames
972+
* intersecting the INP interaction. This includes any end-of-frame style and
973+
* layout duration + any forced style and layout duration.
974+
*/
975+
totalStyleAndLayoutDuration?: number;
976+
/**
977+
* The off main-thread presentation delay from the end of the last Long
978+
* Animation Frame (where available) until the INP end point.
979+
*/
980+
totalPaintDuration?: number;
981+
/**
982+
* The total unattributed time not included in any of the previous totals.
983+
* This includes scripts < 5 milliseconds and other timings not attributed
984+
* by Long Animation Frame (including when a frame is < 50ms and so has no
985+
* Long Animation Frame).
986+
* When no Long Animation Frames are present this will be undefined, rather
987+
* than everything being unattributed to make it clearer when it's expected
988+
* to be small.
989+
*/
990+
totalUnattributedDuration?: number;
991+
}
992+
```
993+
994+
#### `INPLongestScriptSummary`
995+
996+
```ts
997+
interface INPLongestScriptSummary {
998+
/**
999+
* The longest Long Animation Frame script entry that intersects the INP
1000+
* interaction.
1001+
*/
1002+
entry: PerformanceScriptTiming;
1003+
/**
1004+
* The INP subpart where the longest script ran.
1005+
*/
1006+
subpart: 'input-delay' | 'processing-duration' | 'presentation-delay';
1007+
/**
1008+
* The amount of time the longest script intersected the INP duration.
1009+
*/
1010+
intersectingDuration: number;
9581011
}
9591012
```
9601013

@@ -1019,7 +1072,7 @@ interface LCPAttribution {
10191072
#### `TTFBAttribution`
10201073

10211074
```ts
1022-
export interface TTFBAttribution {
1075+
interface TTFBAttribution {
10231076
/**
10241077
* The total time from when the user initiates loading the page to when the
10251078
* page starts to handle the request. Large values here are typically due

src/attribution/onINP.ts

Lines changed: 100 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,9 +23,10 @@ import {whenIdleOrHidden} from '../lib/whenIdleOrHidden.js';
2323
import {onINP as unattributedOnINP} from '../onINP.js';
2424
import {
2525
INPAttribution,
26+
INPAttributionReportOpts,
2627
INPMetric,
2728
INPMetricWithAttribution,
28-
INPAttributionReportOpts,
29+
INPLongestScriptSummary,
2930
} from '../types.js';
3031

3132
interface pendingEntriesGroup {
@@ -238,7 +239,7 @@ export const onINP = (
238239
};
239240

240241
interactionManager._onBeforeProcessingEntry = groupEntriesByRenderTime;
241-
interactionManager._onAfterProcessingInteraction = saveInteractionTarget;
242+
interactionManager._onAfterProcessingINPCandidate = saveInteractionTarget;
242243

243244
const getIntersectingLoAFs = (
244245
start: DOMHighResTimeStamp,
@@ -260,6 +261,96 @@ export const onINP = (
260261
return intersectingLoAFs;
261262
};
262263

264+
const attributeLoAFDetails = (attribution: INPAttribution) => {
265+
// If there is no LoAF data then nothing further to attribute
266+
if (!attribution.longAnimationFrameEntries?.length) {
267+
return;
268+
}
269+
270+
const interactionTime = attribution.interactionTime;
271+
const inputDelay = attribution.inputDelay;
272+
const processingDuration = attribution.processingDuration;
273+
274+
// Stats across all LoAF entries and scripts.
275+
let totalScriptDuration = 0;
276+
let totalStyleAndLayoutDuration = 0;
277+
let totalPaintDuration = 0;
278+
let longestScriptDuration = 0;
279+
let longestScriptEntry: PerformanceScriptTiming | undefined;
280+
let longestScriptSubpart: INPLongestScriptSummary['subpart'] | undefined;
281+
282+
for (const loafEntry of attribution.longAnimationFrameEntries) {
283+
totalStyleAndLayoutDuration =
284+
totalStyleAndLayoutDuration +
285+
loafEntry.startTime +
286+
loafEntry.duration -
287+
loafEntry.styleAndLayoutStart;
288+
289+
for (const script of loafEntry.scripts) {
290+
const scriptEndTime = script.startTime + script.duration;
291+
if (scriptEndTime < interactionTime) {
292+
continue;
293+
}
294+
const intersectingScriptDuration =
295+
scriptEndTime - Math.max(interactionTime, script.startTime);
296+
// Since forcedStyleAndLayoutDuration doesn't provide timestamps, we
297+
// apportion the total based on the intersectingScriptDuration. Not
298+
// correct depending on when it occurred, but the best we can do.
299+
const intersectingForceStyleAndLayoutDuration = script.duration
300+
? (intersectingScriptDuration / script.duration) *
301+
script.forcedStyleAndLayoutDuration
302+
: 0;
303+
// For scripts we exclude forcedStyleAndLayout (same as DevTools does
304+
// in it's summary totals) and instead include that in
305+
// totalStyleAndLayoutDuration
306+
totalScriptDuration +=
307+
intersectingScriptDuration - intersectingForceStyleAndLayoutDuration;
308+
totalStyleAndLayoutDuration += intersectingForceStyleAndLayoutDuration;
309+
310+
if (intersectingScriptDuration > longestScriptDuration) {
311+
// Set the subpart this occurred in.
312+
longestScriptSubpart =
313+
script.startTime < interactionTime + inputDelay
314+
? 'input-delay'
315+
: script.startTime >=
316+
interactionTime + inputDelay + processingDuration
317+
? 'presentation-delay'
318+
: 'processing-duration';
319+
320+
longestScriptEntry = script;
321+
longestScriptDuration = intersectingScriptDuration;
322+
}
323+
}
324+
}
325+
326+
// Calculate the totalPaintDuration from the last LoAF after
327+
// presentationDelay starts (where available)
328+
const lastLoAF = attribution.longAnimationFrameEntries.at(-1);
329+
const lastLoAFEndTime = lastLoAF
330+
? lastLoAF.startTime + lastLoAF.duration
331+
: 0;
332+
if (lastLoAFEndTime >= interactionTime + inputDelay + processingDuration) {
333+
totalPaintDuration = attribution.nextPaintTime - lastLoAFEndTime;
334+
}
335+
336+
if (longestScriptEntry && longestScriptSubpart) {
337+
attribution.longestScript = {
338+
entry: longestScriptEntry,
339+
subpart: longestScriptSubpart,
340+
intersectingDuration: longestScriptDuration,
341+
};
342+
}
343+
attribution.totalScriptDuration = totalScriptDuration;
344+
attribution.totalStyleAndLayoutDuration = totalStyleAndLayoutDuration;
345+
attribution.totalPaintDuration = totalPaintDuration;
346+
attribution.totalUnattributedDuration =
347+
attribution.nextPaintTime -
348+
interactionTime -
349+
totalScriptDuration -
350+
totalStyleAndLayoutDuration -
351+
totalPaintDuration;
352+
};
353+
263354
const attributeINP = (metric: INPMetric): INPMetricWithAttribution => {
264355
const firstEntry = metric.entries[0];
265356
const group = entryToEntriesGroupMap.get(firstEntry)!;
@@ -309,8 +400,15 @@ export const onINP = (
309400
processingDuration: processingEnd - processingStart,
310401
presentationDelay: nextPaintTime - processingEnd,
311402
loadState: getLoadState(firstEntry.startTime),
403+
longestScript: undefined,
404+
totalScriptDuration: undefined,
405+
totalStyleAndLayoutDuration: undefined,
406+
totalPaintDuration: undefined,
407+
totalUnattributedDuration: undefined,
312408
};
313409

410+
attributeLoAFDetails(attribution);
411+
314412
// Use `Object.assign()` to ensure the original metric object is returned.
315413
const metricWithAttribution: INPMetricWithAttribution = Object.assign(
316414
metric,

src/lib/InteractionManager.ts

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,7 @@ export class InteractionManager {
5858

5959
_onBeforeProcessingEntry?: (entry: PerformanceEventTiming) => void;
6060

61-
_onAfterProcessingInteraction?: (interaction: Interaction) => void;
61+
_onAfterProcessingINPCandidate?: (interaction: Interaction) => void;
6262

6363
_resetInteractions() {
6464
prevInteractionCount = getInteractionCount();
@@ -138,8 +138,9 @@ export class InteractionManager {
138138
this._longestInteractionMap.delete(interaction.id);
139139
}
140140
}
141-
}
142141

143-
this._onAfterProcessingInteraction?.(interaction!);
142+
// Call any post-processing on the interaction
143+
this._onAfterProcessingINPCandidate?.(interaction);
144+
}
144145
}
145146
}

src/types.ts

Lines changed: 46 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -88,9 +88,53 @@ declare global {
8888
readonly element: Element | null;
8989
}
9090

91+
// https://w3c.github.io/long-animation-frame/#sec-PerformanceLongAnimationFrameTiming
92+
export type ScriptInvokerType =
93+
| 'classic-script'
94+
| 'module-script'
95+
| 'event-listener'
96+
| 'user-callback'
97+
| 'resolve-promise'
98+
| 'reject-promise';
99+
100+
// https://w3c.github.io/long-animation-frame/#sec-PerformanceLongAnimationFrameTiming
101+
export type ScriptWindowAttribution =
102+
| 'self'
103+
| 'descendant'
104+
| 'ancestor'
105+
| 'same-page'
106+
| 'other';
107+
108+
// https://w3c.github.io/long-animation-frame/#sec-PerformanceLongAnimationFrameTiming
109+
interface PerformanceScriptTiming extends PerformanceEntry {
110+
/* Overloading PerformanceEntry */
111+
readonly startTime: DOMHighResTimeStamp;
112+
readonly duration: DOMHighResTimeStamp;
113+
readonly name: string;
114+
readonly entryType: string;
115+
116+
readonly invokerType: ScriptInvokerType;
117+
readonly invoker: string;
118+
readonly executionStart: DOMHighResTimeStamp;
119+
readonly sourceURL: string;
120+
readonly sourceFunctionName: string;
121+
readonly sourceCharPosition: number;
122+
readonly pauseDuration: DOMHighResTimeStamp;
123+
readonly forcedStyleAndLayoutDuration: DOMHighResTimeStamp;
124+
readonly window?: Window;
125+
readonly windowAttribution: ScriptWindowAttribution;
126+
}
127+
91128
// https://w3c.github.io/long-animation-frame/#sec-PerformanceLongAnimationFrameTiming
92129
interface PerformanceLongAnimationFrameTiming extends PerformanceEntry {
93-
renderStart: DOMHighResTimeStamp;
94-
duration: DOMHighResTimeStamp;
130+
readonly startTime: DOMHighResTimeStamp;
131+
readonly duration: DOMHighResTimeStamp;
132+
readonly name: string;
133+
readonly entryType: string;
134+
readonly renderStart: DOMHighResTimeStamp;
135+
readonly styleAndLayoutStart: DOMHighResTimeStamp;
136+
readonly blockingDuration: DOMHighResTimeStamp;
137+
readonly firstUIEventTimestamp: DOMHighResTimeStamp;
138+
readonly scripts: PerformanceScriptTiming[];
95139
}
96140
}

0 commit comments

Comments
 (0)