added orai

This commit is contained in:
szabomarton
2024-12-09 13:11:41 +01:00
parent 24a1542fff
commit bc5e7d251a
168 changed files with 24419 additions and 0 deletions

View File

@@ -0,0 +1,7 @@
export {
/**
* @deprecated Use `onINP()` instead.
*/
onFID, } from './onFID.js';
export { FIDThresholds } from '../onFID.js';
export * from '../types.js';

View File

@@ -0,0 +1,22 @@
/*
* Copyright 2024 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
export {
/**
* @deprecated Use `onINP()` instead.
*/
onFID, } from './onFID.js';
export { FIDThresholds } from '../onFID.js';
export * from '../types.js';

View File

@@ -0,0 +1,12 @@
export { onCLS } from './onCLS.js';
export { onFCP } from './onFCP.js';
export { onINP } from './onINP.js';
export { onLCP } from './onLCP.js';
export { onTTFB } from './onTTFB.js';
export { CLSThresholds } from '../onCLS.js';
export { FCPThresholds } from '../onFCP.js';
export { INPThresholds } from '../onINP.js';
export { LCPThresholds } from '../onLCP.js';
export { TTFBThresholds } from '../onTTFB.js';
export * from './deprecated.js';
export * from '../types.js';

View File

@@ -0,0 +1,27 @@
/*
* Copyright 2022 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
export { onCLS } from './onCLS.js';
export { onFCP } from './onFCP.js';
export { onINP } from './onINP.js';
export { onLCP } from './onLCP.js';
export { onTTFB } from './onTTFB.js';
export { CLSThresholds } from '../onCLS.js';
export { FCPThresholds } from '../onFCP.js';
export { INPThresholds } from '../onINP.js';
export { LCPThresholds } from '../onLCP.js';
export { TTFBThresholds } from '../onTTFB.js';
export * from './deprecated.js';
export * from '../types.js';

View File

@@ -0,0 +1,23 @@
import { CLSMetricWithAttribution, ReportOpts } from '../types.js';
/**
* Calculates the [CLS](https://web.dev/articles/cls) value for the current page and
* calls the `callback` function once the value is ready to be reported, along
* with all `layout-shift` performance entries that were used in the metric
* value calculation. The reported value is a `double` (corresponding to a
* [layout shift score](https://web.dev/articles/cls#layout_shift_score)).
*
* If the `reportAllChanges` configuration option is set to `true`, the
* `callback` function will be called as soon as the value is initially
* determined as well as any time the value changes throughout the page
* lifespan.
*
* _**Important:** CLS should be continually monitored for changes throughout
* the entire lifespan of a page—including if the user returns to the page after
* it's been hidden/backgrounded. However, since browsers often [will not fire
* additional callbacks once the user has backgrounded a
* page](https://developer.chrome.com/blog/page-lifecycle-api/#advice-hidden),
* `callback` is always called when the page's visibility state changes to
* hidden. As a result, the `callback` function might be called multiple times
* during the same page load._
*/
export declare const onCLS: (onReport: (metric: CLSMetricWithAttribution) => void, opts?: ReportOpts) => void;

View File

@@ -0,0 +1,74 @@
/*
* Copyright 2022 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { getLoadState } from '../lib/getLoadState.js';
import { getSelector } from '../lib/getSelector.js';
import { onCLS as unattributedOnCLS } from '../onCLS.js';
const getLargestLayoutShiftEntry = (entries) => {
return entries.reduce((a, b) => (a && a.value > b.value ? a : b));
};
const getLargestLayoutShiftSource = (sources) => {
return sources.find((s) => s.node && s.node.nodeType === 1) || sources[0];
};
const attributeCLS = (metric) => {
// Use an empty object if no other attribution has been set.
let attribution = {};
if (metric.entries.length) {
const largestEntry = getLargestLayoutShiftEntry(metric.entries);
if (largestEntry && largestEntry.sources && largestEntry.sources.length) {
const largestSource = getLargestLayoutShiftSource(largestEntry.sources);
if (largestSource) {
attribution = {
largestShiftTarget: getSelector(largestSource.node),
largestShiftTime: largestEntry.startTime,
largestShiftValue: largestEntry.value,
largestShiftSource: largestSource,
largestShiftEntry: largestEntry,
loadState: getLoadState(largestEntry.startTime),
};
}
}
}
// Use Object.assign to set property to keep tsc happy.
const metricWithAttribution = Object.assign(metric, { attribution });
return metricWithAttribution;
};
/**
* Calculates the [CLS](https://web.dev/articles/cls) value for the current page and
* calls the `callback` function once the value is ready to be reported, along
* with all `layout-shift` performance entries that were used in the metric
* value calculation. The reported value is a `double` (corresponding to a
* [layout shift score](https://web.dev/articles/cls#layout_shift_score)).
*
* If the `reportAllChanges` configuration option is set to `true`, the
* `callback` function will be called as soon as the value is initially
* determined as well as any time the value changes throughout the page
* lifespan.
*
* _**Important:** CLS should be continually monitored for changes throughout
* the entire lifespan of a page—including if the user returns to the page after
* it's been hidden/backgrounded. However, since browsers often [will not fire
* additional callbacks once the user has backgrounded a
* page](https://developer.chrome.com/blog/page-lifecycle-api/#advice-hidden),
* `callback` is always called when the page's visibility state changes to
* hidden. As a result, the `callback` function might be called multiple times
* during the same page load._
*/
export const onCLS = (onReport, opts) => {
unattributedOnCLS((metric) => {
const metricWithAttribution = attributeCLS(metric);
onReport(metricWithAttribution);
}, opts);
};

View File

@@ -0,0 +1,8 @@
import { FCPMetricWithAttribution, ReportOpts } from '../types.js';
/**
* Calculates the [FCP](https://web.dev/articles/fcp) value for the current page and
* calls the `callback` function once the value is ready, along with the
* relevant `paint` performance entry used to determine the value. The reported
* value is a `DOMHighResTimeStamp`.
*/
export declare const onFCP: (onReport: (metric: FCPMetricWithAttribution) => void, opts?: ReportOpts) => void;

View File

@@ -0,0 +1,57 @@
/*
* Copyright 2022 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { getBFCacheRestoreTime } from '../lib/bfcache.js';
import { getLoadState } from '../lib/getLoadState.js';
import { getNavigationEntry } from '../lib/getNavigationEntry.js';
import { onFCP as unattributedOnFCP } from '../onFCP.js';
const attributeFCP = (metric) => {
// Use a default object if no other attribution has been set.
let attribution = {
timeToFirstByte: 0,
firstByteToFCP: metric.value,
loadState: getLoadState(getBFCacheRestoreTime()),
};
if (metric.entries.length) {
const navigationEntry = getNavigationEntry();
const fcpEntry = metric.entries[metric.entries.length - 1];
if (navigationEntry) {
const activationStart = navigationEntry.activationStart || 0;
const ttfb = Math.max(0, navigationEntry.responseStart - activationStart);
attribution = {
timeToFirstByte: ttfb,
firstByteToFCP: metric.value - ttfb,
loadState: getLoadState(metric.entries[0].startTime),
navigationEntry,
fcpEntry,
};
}
}
// Use Object.assign to set property to keep tsc happy.
const metricWithAttribution = Object.assign(metric, { attribution });
return metricWithAttribution;
};
/**
* Calculates the [FCP](https://web.dev/articles/fcp) value for the current page and
* calls the `callback` function once the value is ready, along with the
* relevant `paint` performance entry used to determine the value. The reported
* value is a `DOMHighResTimeStamp`.
*/
export const onFCP = (onReport, opts) => {
unattributedOnFCP((metric) => {
const metricWithAttribution = attributeFCP(metric);
onReport(metricWithAttribution);
}, opts);
};

View File

@@ -0,0 +1,11 @@
import { FIDMetricWithAttribution, ReportOpts } from '../types.js';
/**
* Calculates the [FID](https://web.dev/articles/fid) value for the current page and
* calls the `callback` function once the value is ready, along with the
* relevant `first-input` performance entry used to determine the value. The
* reported value is a `DOMHighResTimeStamp`.
*
* _**Important:** since FID is only reported after the user interacts with the
* page, it's possible that it will not be reported for some page loads._
*/
export declare const onFID: (onReport: (metric: FIDMetricWithAttribution) => void, opts?: ReportOpts) => void;

View File

@@ -0,0 +1,46 @@
/*
* Copyright 2022 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { getLoadState } from '../lib/getLoadState.js';
import { getSelector } from '../lib/getSelector.js';
import { onFID as unattributedOnFID } from '../onFID.js';
const attributeFID = (metric) => {
const fidEntry = metric.entries[0];
const attribution = {
eventTarget: getSelector(fidEntry.target),
eventType: fidEntry.name,
eventTime: fidEntry.startTime,
eventEntry: fidEntry,
loadState: getLoadState(fidEntry.startTime),
};
// Use Object.assign to set property to keep tsc happy.
const metricWithAttribution = Object.assign(metric, { attribution });
return metricWithAttribution;
};
/**
* Calculates the [FID](https://web.dev/articles/fid) value for the current page and
* calls the `callback` function once the value is ready, along with the
* relevant `first-input` performance entry used to determine the value. The
* reported value is a `DOMHighResTimeStamp`.
*
* _**Important:** since FID is only reported after the user interacts with the
* page, it's possible that it will not be reported for some page loads._
*/
export const onFID = (onReport, opts) => {
unattributedOnFID((metric) => {
const metricWithAttribution = attributeFID(metric);
onReport(metricWithAttribution);
}, opts);
};

View File

@@ -0,0 +1,30 @@
import { INPMetricWithAttribution, ReportOpts } from '../types.js';
export declare const interactionTargetMap: Map<number, Node>;
/**
* Calculates the [INP](https://web.dev/articles/inp) value for the current
* page and calls the `callback` function once the value is ready, along with
* the `event` performance entries reported for that interaction. The reported
* value is a `DOMHighResTimeStamp`.
*
* A custom `durationThreshold` configuration option can optionally be passed to
* control what `event-timing` entries are considered for INP reporting. The
* default threshold is `40`, which means INP scores of less than 40 are
* reported as 0. Note that this will not affect your 75th percentile INP value
* unless that value is also less than 40 (well below the recommended
* [good](https://web.dev/articles/inp#what_is_a_good_inp_score) threshold).
*
* If the `reportAllChanges` configuration option is set to `true`, the
* `callback` function will be called as soon as the value is initially
* determined as well as any time the value changes throughout the page
* lifespan.
*
* _**Important:** INP should be continually monitored for changes throughout
* the entire lifespan of a page—including if the user returns to the page after
* it's been hidden/backgrounded. However, since browsers often [will not fire
* additional callbacks once the user has backgrounded a
* page](https://developer.chrome.com/blog/page-lifecycle-api/#advice-hidden),
* `callback` is always called when the page's visibility state changes to
* hidden. As a result, the `callback` function might be called multiple times
* during the same page load._
*/
export declare const onINP: (onReport: (metric: INPMetricWithAttribution) => void, opts?: ReportOpts) => void;

View File

@@ -0,0 +1,256 @@
/*
* Copyright 2022 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { getLoadState } from '../lib/getLoadState.js';
import { getSelector } from '../lib/getSelector.js';
import { longestInteractionList, entryPreProcessingCallbacks, longestInteractionMap, } from '../lib/interactions.js';
import { observe } from '../lib/observe.js';
import { whenIdle } from '../lib/whenIdle.js';
import { onINP as unattributedOnINP } from '../onINP.js';
// The maximum number of previous frames for which data is kept.
// Storing data about previous frames is necessary to handle cases where event
// and LoAF entries are dispatched out of order, and so a buffer of previous
// frame data is needed to determine various bits of INP attribution once all
// the frame-related data has come in.
// In most cases this out-of-order data is only off by a frame or two, so
// keeping the most recent 50 should be more than sufficient.
const MAX_PREVIOUS_FRAMES = 50;
// A PerformanceObserver, observing new `long-animation-frame` entries.
// If this variable is defined it means the browser supports LoAF.
let loafObserver;
// A list of LoAF entries that have been dispatched and could potentially
// intersect with the INP candidate interaction. Note that periodically this
// list is cleaned up and entries that are known to not match INP are removed.
let pendingLoAFs = [];
// An array of groups of all the event timing entries that occurred within a
// particular frame. Note that periodically this array is cleaned up and entries
// that are known to not match INP are removed.
let pendingEntriesGroups = [];
// The `processingEnd` time of most recently-processed event, chronologically.
let latestProcessingEnd = 0;
// A WeakMap to look up the event-timing-entries group of a given entry.
// Note that this only maps from "important" entries: either the first input or
// those with an `interactionId`.
const entryToEntriesGroupMap = new WeakMap();
// A mapping of interactionIds to the target Node.
export const interactionTargetMap = 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.
let idleHandle = -1;
/**
* Adds new LoAF entries to the `pendingLoAFs` list.
*/
const handleLoAFEntries = (entries) => {
pendingLoAFs = pendingLoAFs.concat(entries);
queueCleanup();
};
// Get a reference to the interaction target element in case it's removed
// from the DOM later.
const saveInteractionTarget = (entry) => {
if (entry.interactionId &&
entry.target &&
!interactionTargetMap.has(entry.interactionId)) {
interactionTargetMap.set(entry.interactionId, entry.target);
}
};
/**
* Groups entries that were presented within the same animation frame by
* a common `renderTime`. This function works by referencing
* `pendingEntriesGroups` and using an existing render time if one is found
* (otherwise creating a new one). This function also adds all interaction
* entries to an `entryToRenderTimeMap` WeakMap so that the "grouped" entries
* can be looked up later.
*/
const groupEntriesByRenderTime = (entry) => {
const renderTime = entry.startTime + entry.duration;
let group;
latestProcessingEnd = Math.max(latestProcessingEnd, entry.processingEnd);
// Iterate over all previous render times in reverse order to find a match.
// Go in reverse since the most likely match will be at the end.
for (let i = pendingEntriesGroups.length - 1; i >= 0; i--) {
const potentialGroup = pendingEntriesGroups[i];
// If a group's render time is within 8ms of the entry's render time,
// assume they were part of the same frame and add it to the group.
if (Math.abs(renderTime - potentialGroup.renderTime) <= 8) {
group = potentialGroup;
group.startTime = Math.min(entry.startTime, group.startTime);
group.processingStart = Math.min(entry.processingStart, group.processingStart);
group.processingEnd = Math.max(entry.processingEnd, group.processingEnd);
group.entries.push(entry);
break;
}
}
// If there was no matching group, assume this is a new frame.
if (!group) {
group = {
startTime: entry.startTime,
processingStart: entry.processingStart,
processingEnd: entry.processingEnd,
renderTime,
entries: [entry],
};
pendingEntriesGroups.push(group);
}
// Store the grouped render time for this entry for reference later.
if (entry.interactionId || entry.entryType === 'first-input') {
entryToEntriesGroupMap.set(entry, group);
}
queueCleanup();
};
const queueCleanup = () => {
// Queue cleanup of entries that are not part of any INP candidates.
if (idleHandle < 0) {
idleHandle = whenIdle(cleanupEntries);
}
};
const cleanupEntries = () => {
// Delete any stored interaction target elements if they're not part of one
// of the 10 longest interactions.
if (interactionTargetMap.size > 10) {
interactionTargetMap.forEach((_, key) => {
if (!longestInteractionMap.has(key)) {
interactionTargetMap.delete(key);
}
});
}
// Keep all render times that are part of a pending INP candidate or
// that occurred within the 50 most recently-dispatched groups of events.
const longestInteractionGroups = longestInteractionList.map((i) => {
return entryToEntriesGroupMap.get(i.entries[0]);
});
const minIndex = pendingEntriesGroups.length - MAX_PREVIOUS_FRAMES;
pendingEntriesGroups = pendingEntriesGroups.filter((group, index) => {
if (index >= minIndex)
return true;
return longestInteractionGroups.includes(group);
});
// Keep all pending LoAF entries that either:
// 1) intersect with entries in the newly cleaned up `pendingEntriesGroups`
// 2) occur after the most recently-processed event entry (for up to MAX_PREVIOUS_FRAMES)
const loafsToKeep = new Set();
for (let i = 0; i < pendingEntriesGroups.length; i++) {
const group = pendingEntriesGroups[i];
getIntersectingLoAFs(group.startTime, group.processingEnd).forEach((loaf) => {
loafsToKeep.add(loaf);
});
}
const prevFrameIndexCutoff = pendingLoAFs.length - 1 - MAX_PREVIOUS_FRAMES;
// Filter `pendingLoAFs` to preserve LoAF order.
pendingLoAFs = pendingLoAFs.filter((loaf, index) => {
if (loaf.startTime > latestProcessingEnd && index > prevFrameIndexCutoff) {
return true;
}
return loafsToKeep.has(loaf);
});
// Reset the idle callback handle so it can be queued again.
idleHandle = -1;
};
entryPreProcessingCallbacks.push(saveInteractionTarget, groupEntriesByRenderTime);
const getIntersectingLoAFs = (start, end) => {
const intersectingLoAFs = [];
for (let i = 0, loaf; (loaf = pendingLoAFs[i]); i++) {
// If the LoAF ends before the given start time, ignore it.
if (loaf.startTime + loaf.duration < start)
continue;
// If the LoAF starts after the given end time, ignore it and all
// subsequent pending LoAFs (because they're in time order).
if (loaf.startTime > end)
break;
// Still here? If so this LoAF intersects with the interaction.
intersectingLoAFs.push(loaf);
}
return intersectingLoAFs;
};
const attributeINP = (metric) => {
const firstEntry = metric.entries[0];
const group = entryToEntriesGroupMap.get(firstEntry);
const processingStart = firstEntry.processingStart;
const processingEnd = group.processingEnd;
// Sort the entries in processing time order.
const processedEventEntries = group.entries.sort((a, b) => {
return a.processingStart - b.processingStart;
});
const longAnimationFrameEntries = getIntersectingLoAFs(firstEntry.startTime, processingEnd);
// The first interaction entry may not have a target defined, so use the
// 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
// As a fallback, also check the interactionTargetMap (to account for
// cases where the element is removed from the DOM before reporting happens).
const firstEntryWithTarget = metric.entries.find((entry) => entry.target);
const interactionTargetElement = (firstEntryWithTarget && 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
// end time of any LoAF entry.
const nextPaintTimeCandidates = [
firstEntry.startTime + firstEntry.duration,
processingEnd,
].concat(longAnimationFrameEntries.map((loaf) => loaf.startTime + loaf.duration));
const nextPaintTime = Math.max.apply(Math, nextPaintTimeCandidates);
const attribution = {
interactionTarget: getSelector(interactionTargetElement),
interactionTargetElement: interactionTargetElement,
interactionType: firstEntry.name.startsWith('key') ? 'keyboard' : 'pointer',
interactionTime: firstEntry.startTime,
nextPaintTime: nextPaintTime,
processedEventEntries: processedEventEntries,
longAnimationFrameEntries: longAnimationFrameEntries,
inputDelay: processingStart - firstEntry.startTime,
processingDuration: processingEnd - processingStart,
presentationDelay: Math.max(nextPaintTime - processingEnd, 0),
loadState: getLoadState(firstEntry.startTime),
};
// Use Object.assign to set property to keep tsc happy.
const metricWithAttribution = Object.assign(metric, { attribution });
return metricWithAttribution;
};
/**
* Calculates the [INP](https://web.dev/articles/inp) value for the current
* page and calls the `callback` function once the value is ready, along with
* the `event` performance entries reported for that interaction. The reported
* value is a `DOMHighResTimeStamp`.
*
* A custom `durationThreshold` configuration option can optionally be passed to
* control what `event-timing` entries are considered for INP reporting. The
* default threshold is `40`, which means INP scores of less than 40 are
* reported as 0. Note that this will not affect your 75th percentile INP value
* unless that value is also less than 40 (well below the recommended
* [good](https://web.dev/articles/inp#what_is_a_good_inp_score) threshold).
*
* If the `reportAllChanges` configuration option is set to `true`, the
* `callback` function will be called as soon as the value is initially
* determined as well as any time the value changes throughout the page
* lifespan.
*
* _**Important:** INP should be continually monitored for changes throughout
* the entire lifespan of a page—including if the user returns to the page after
* it's been hidden/backgrounded. However, since browsers often [will not fire
* additional callbacks once the user has backgrounded a
* page](https://developer.chrome.com/blog/page-lifecycle-api/#advice-hidden),
* `callback` is always called when the page's visibility state changes to
* hidden. As a result, the `callback` function might be called multiple times
* during the same page load._
*/
export const onINP = (onReport, opts) => {
if (!loafObserver) {
loafObserver = observe('long-animation-frame', handleLoAFEntries);
}
unattributedOnINP((metric) => {
const metricWithAttribution = attributeINP(metric);
onReport(metricWithAttribution);
}, opts);
};

View File

@@ -0,0 +1,13 @@
import { LCPMetricWithAttribution, ReportOpts } from '../types.js';
/**
* Calculates the [LCP](https://web.dev/articles/lcp) value for the current page and
* calls the `callback` function once the value is ready (along with the
* relevant `largest-contentful-paint` performance entry used to determine the
* value). The reported value is a `DOMHighResTimeStamp`.
*
* If the `reportAllChanges` configuration option is set to `true`, the
* `callback` function will be called any time a new `largest-contentful-paint`
* performance entry is dispatched, or once the final value of the metric has
* been determined.
*/
export declare const onLCP: (onReport: (metric: LCPMetricWithAttribution) => void, opts?: ReportOpts) => void;

View File

@@ -0,0 +1,83 @@
/*
* Copyright 2022 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { getNavigationEntry } from '../lib/getNavigationEntry.js';
import { getSelector } from '../lib/getSelector.js';
import { onLCP as unattributedOnLCP } from '../onLCP.js';
const attributeLCP = (metric) => {
// Use a default object if no other attribution has been set.
let attribution = {
timeToFirstByte: 0,
resourceLoadDelay: 0,
resourceLoadDuration: 0,
elementRenderDelay: metric.value,
};
if (metric.entries.length) {
const navigationEntry = getNavigationEntry();
if (navigationEntry) {
const activationStart = navigationEntry.activationStart || 0;
const lcpEntry = metric.entries[metric.entries.length - 1];
const lcpResourceEntry = lcpEntry.url &&
performance
.getEntriesByType('resource')
.filter((e) => e.name === lcpEntry.url)[0];
const ttfb = Math.max(0, navigationEntry.responseStart - activationStart);
const lcpRequestStart = Math.max(ttfb,
// Prefer `requestStart` (if TOA is set), otherwise use `startTime`.
lcpResourceEntry
? (lcpResourceEntry.requestStart || lcpResourceEntry.startTime) -
activationStart
: 0);
const lcpResponseEnd = Math.max(lcpRequestStart, lcpResourceEntry ? lcpResourceEntry.responseEnd - activationStart : 0);
const lcpRenderTime = Math.max(lcpResponseEnd, lcpEntry.startTime - activationStart);
attribution = {
element: getSelector(lcpEntry.element),
timeToFirstByte: ttfb,
resourceLoadDelay: lcpRequestStart - ttfb,
resourceLoadDuration: lcpResponseEnd - lcpRequestStart,
elementRenderDelay: lcpRenderTime - lcpResponseEnd,
navigationEntry,
lcpEntry,
};
// Only attribution the URL and resource entry if they exist.
if (lcpEntry.url) {
attribution.url = lcpEntry.url;
}
if (lcpResourceEntry) {
attribution.lcpResourceEntry = lcpResourceEntry;
}
}
}
// Use Object.assign to set property to keep tsc happy.
const metricWithAttribution = Object.assign(metric, { attribution });
return metricWithAttribution;
};
/**
* Calculates the [LCP](https://web.dev/articles/lcp) value for the current page and
* calls the `callback` function once the value is ready (along with the
* relevant `largest-contentful-paint` performance entry used to determine the
* value). The reported value is a `DOMHighResTimeStamp`.
*
* If the `reportAllChanges` configuration option is set to `true`, the
* `callback` function will be called any time a new `largest-contentful-paint`
* performance entry is dispatched, or once the final value of the metric has
* been determined.
*/
export const onLCP = (onReport, opts) => {
unattributedOnLCP((metric) => {
const metricWithAttribution = attributeLCP(metric);
onReport(metricWithAttribution);
}, opts);
};

View File

@@ -0,0 +1,17 @@
import { TTFBMetricWithAttribution, ReportOpts } from '../types.js';
/**
* Calculates the [TTFB](https://web.dev/articles/ttfb) value for the
* current page and calls the `callback` function once the page has loaded,
* along with the relevant `navigation` performance entry used to determine the
* value. The reported value is a `DOMHighResTimeStamp`.
*
* Note, this function waits until after the page is loaded to call `callback`
* in order to ensure all properties of the `navigation` entry are populated.
* This is useful if you want to report on other metrics exposed by the
* [Navigation Timing API](https://w3c.github.io/navigation-timing/). For
* example, the TTFB metric starts from the page's [time
* origin](https://www.w3.org/TR/hr-time-2/#sec-time-origin), which means it
* includes time spent on DNS lookup, connection negotiation, network latency,
* and server processing time.
*/
export declare const onTTFB: (onReport: (metric: TTFBMetricWithAttribution) => void, opts?: ReportOpts) => void;

View File

@@ -0,0 +1,76 @@
/*
* Copyright 2022 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { onTTFB as unattributedOnTTFB } from '../onTTFB.js';
const attributeTTFB = (metric) => {
// Use a default object if no other attribution has been set.
let attribution = {
waitingDuration: 0,
cacheDuration: 0,
dnsDuration: 0,
connectionDuration: 0,
requestDuration: 0,
};
if (metric.entries.length) {
const navigationEntry = metric.entries[0];
const activationStart = navigationEntry.activationStart || 0;
// Measure from workerStart or fetchStart so any service worker startup
// time is included in cacheDuration (which also includes other sw time
// anyway, that cannot be accurately split out cross-browser).
const waitEnd = Math.max((navigationEntry.workerStart || navigationEntry.fetchStart) -
activationStart, 0);
const dnsStart = Math.max(navigationEntry.domainLookupStart - activationStart, 0);
const connectStart = Math.max(navigationEntry.connectStart - activationStart, 0);
const connectEnd = Math.max(navigationEntry.connectEnd - activationStart, 0);
attribution = {
waitingDuration: waitEnd,
cacheDuration: dnsStart - waitEnd,
// dnsEnd usually equals connectStart but use connectStart over dnsEnd
// for dnsDuration in case there ever is a gap.
dnsDuration: connectStart - dnsStart,
connectionDuration: connectEnd - connectStart,
// There is often a gap between connectEnd and requestStart. Attribute
// that to requestDuration so connectionDuration remains 0 for
// service worker controlled requests were connectStart and connectEnd
// are the same.
requestDuration: metric.value - connectEnd,
navigationEntry: navigationEntry,
};
}
// Use Object.assign to set property to keep tsc happy.
const metricWithAttribution = Object.assign(metric, { attribution });
return metricWithAttribution;
};
/**
* Calculates the [TTFB](https://web.dev/articles/ttfb) value for the
* current page and calls the `callback` function once the page has loaded,
* along with the relevant `navigation` performance entry used to determine the
* value. The reported value is a `DOMHighResTimeStamp`.
*
* Note, this function waits until after the page is loaded to call `callback`
* in order to ensure all properties of the `navigation` entry are populated.
* This is useful if you want to report on other metrics exposed by the
* [Navigation Timing API](https://w3c.github.io/navigation-timing/). For
* example, the TTFB metric starts from the page's [time
* origin](https://www.w3.org/TR/hr-time-2/#sec-time-origin), which means it
* includes time spent on DNS lookup, connection negotiation, network latency,
* and server processing time.
*/
export const onTTFB = (onReport, opts) => {
unattributedOnTTFB((metric) => {
const metricWithAttribution = attributeTTFB(metric);
onReport(metricWithAttribution);
}, opts);
};