UNPKG

web-vitals

Version:

Easily measure performance metrics in JavaScript

147 lines (126 loc) 5.41 kB
/* * 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. */ import {getInteractionCount} from './polyfills/interactionCountPolyfill.js'; export interface Interaction { _latency: number; // While the `id` and `entries` properties are also internal and could be // mangled by prefixing with an underscore, since they correspond to public // symbols there is no need to mangle them as the library will compress // better if we reuse the existing names. id: number; entries: PerformanceEventTiming[]; } // To prevent unnecessary memory usage on pages with lots of interactions, // store at most 10 of the longest interactions to consider as INP candidates. const MAX_INTERACTIONS_TO_CONSIDER = 10; // Used to store the interaction count after a bfcache restore, since p98 // interaction latencies should only consider the current navigation. let prevInteractionCount = 0; /** * Returns the interaction count since the last bfcache restore (or for the * full page lifecycle if there were no bfcache restores). */ const getInteractionCountForNavigation = () => { return getInteractionCount() - prevInteractionCount; }; export class InteractionManager { /** * A list of longest interactions on the page (by latency) sorted so the * longest one is first. The list is at most MAX_INTERACTIONS_TO_CONSIDER * long. */ _longestInteractionList: Interaction[] = []; /** * A mapping of longest interactions by their interaction ID. * This is used for faster lookup. */ _longestInteractionMap: Map<number, Interaction> = new Map(); _onBeforeProcessingEntry?: (entry: PerformanceEventTiming) => void; _onAfterProcessingINPCandidate?: (interaction: Interaction) => void; _resetInteractions() { prevInteractionCount = getInteractionCount(); this._longestInteractionList.length = 0; this._longestInteractionMap.clear(); } /** * Returns the estimated p98 longest interaction based on the stored * interaction candidates and the interaction count for the current page. */ _estimateP98LongestInteraction() { const candidateInteractionIndex = Math.min( this._longestInteractionList.length - 1, Math.floor(getInteractionCountForNavigation() / 50), ); return this._longestInteractionList[candidateInteractionIndex]; } /** * Takes a performance entry and adds it to the list of worst interactions * if its duration is long enough to make it among the worst. If the * entry is part of an existing interaction, it is merged and the latency * and entries list is updated as needed. */ _processEntry(entry: PerformanceEventTiming) { this._onBeforeProcessingEntry?.(entry); // Skip further processing for entries that cannot be INP candidates. if (!(entry.interactionId || entry.entryType === 'first-input')) return; // The least-long of the 10 longest interactions. const minLongestInteraction = this._longestInteractionList.at(-1); let interaction = this._longestInteractionMap.get(entry.interactionId!); // Only process the entry if it's possibly one of the ten longest, // or if it's part of an existing interaction. if ( interaction || this._longestInteractionList.length < MAX_INTERACTIONS_TO_CONSIDER || // If the above conditions are false, `minLongestInteraction` will be set. entry.duration > minLongestInteraction!._latency ) { // If the interaction already exists, update it. Otherwise create one. if (interaction) { // If the new entry has a longer duration, replace the old entries, // otherwise add to the array. if (entry.duration > interaction._latency) { interaction.entries = [entry]; interaction._latency = entry.duration; } else if ( entry.duration === interaction._latency && entry.startTime === interaction.entries[0].startTime ) { interaction.entries.push(entry); } } else { interaction = { id: entry.interactionId!, entries: [entry], _latency: entry.duration, }; this._longestInteractionMap.set(interaction.id, interaction); this._longestInteractionList.push(interaction); } // Sort the entries by latency (descending) and keep only the top ten. this._longestInteractionList.sort((a, b) => b._latency - a._latency); if (this._longestInteractionList.length > MAX_INTERACTIONS_TO_CONSIDER) { const removedInteractions = this._longestInteractionList.splice( MAX_INTERACTIONS_TO_CONSIDER, ); for (const interaction of removedInteractions) { this._longestInteractionMap.delete(interaction.id); } } // Call any post-processing on the interaction this._onAfterProcessingINPCandidate?.(interaction); } } }