UNPKG

web-vitals

Version:

Easily measure performance metrics in JavaScript

112 lines (111 loc) 5.21 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'; // 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 = []; /** * A mapping of longest interactions by their interaction ID. * This is used for faster lookup. */ _longestInteractionMap = new Map(); _onBeforeProcessingEntry; _onAfterProcessingINPCandidate; _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) { 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); } } }