UNPKG

web-vitals

Version:

Easily measure performance metrics in JavaScript

193 lines 10.3 kB
/* * Copyright 2020 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 { LCPEntryManager } from './lib/LCPEntryManager.js'; import { getBFCacheRestoreTime, onBFCacheRestore } from './lib/bfcache.js'; import { bindReporter } from './lib/bindReporter.js'; import { doubleRAF } from './lib/doubleRAF.js'; import { getActivationStart } from './lib/getActivationStart.js'; import { checkSoftNavsEnabled, storeSoftNavEntry } from './lib/softNavs.js'; import { getVisibilityWatcher } from './lib/getVisibilityWatcher.js'; import { initMetric } from './lib/initMetric.js'; import { initUnique } from './lib/initUnique.js'; import { observe } from './lib/observe.js'; import { whenActivated } from './lib/whenActivated.js'; import { whenIdleOrHidden } from './lib/whenIdleOrHidden.js'; /** Thresholds for LCP. See https://web.dev/articles/lcp#what_is_a_good_lcp_score */ export const LCPThresholds = [2500, 4000]; /** * 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 = {}) => { // As InteractionContentfulPaint entries used by soft navs can emit after // LCP is finalized, we need a flag to know to ignore them. let isFinalized = false; const softNavsEnabled = checkSoftNavsEnabled(opts); whenActivated(() => { let visibilityWatcher = getVisibilityWatcher(); let metric = initMetric('LCP'); let report; const lcpEntryManager = initUnique(opts, LCPEntryManager); const initNewLCPMetric = (navigation, navigationId, navigationInteractionId, navigationURL, navigationStartTime) => { metric = initMetric('LCP', -1, navigation, navigationId, navigationInteractionId, navigationURL, navigationStartTime); report = bindReporter(onReport, metric, LCPThresholds, opts.reportAllChanges); // Reset the finalized flag isFinalized = false; // If it's a soft nav, then need to reset the visibilityWatcher if (navigation === 'soft-navigation') { visibilityWatcher = getVisibilityWatcher(true); } }; const handleSoftNavEntry = (entry) => { if (lcpEntryManager._softNavigationEntryMap && entry.navigationId) { storeSoftNavEntry(lcpEntryManager._softNavigationEntryMap, entry); } if (!isFinalized) report(true); initNewLCPMetric('soft-navigation', entry.navigationId, entry.interactionId, entry.name, entry.startTime); // Soft Navs should contain the largest paint until now, so handle that // as if it just happened, then listen for more. // It can however be null in rare circumstances // (see https://github.com/GoogleChrome/web-vitals/issues/725) const largestInteractionContentfulPaint = entry.getLargestInteractionContentfulPaint?.(); if (largestInteractionContentfulPaint) { handleEntries([largestInteractionContentfulPaint]); } }; const handleEntries = (entries) => { // If reportAllChanges is set or soft navs is enabled then call this // function for each entry, otherwise only consider the last one. if (!opts.reportAllChanges && !softNavsEnabled) { entries = entries.slice(-1); } for (const entry of entries) { if (!entry) continue; if (entry.entryType === 'soft-navigation') { handleSoftNavEntry(entry); continue; } let value = 0; let metricEntries = []; let renderTime = entry.startTime; if (entry.entryType === 'largest-contentful-paint') { // The startTime attribute returns the value of the renderTime if it is // not 0, and the value of the loadTime otherwise. The activationStart // reference is used because LCP should be relative to page activation // rather than navigation start if the page was prerendered. But in cases // where `activationStart` occurs after the LCP, this time should be // clamped at 0. value = Math.max(entry.startTime - getActivationStart(), 0); lcpEntryManager._processEntry(entry); metricEntries = [entry]; } else if (entry.entryType === 'interaction-contentful-paint') { const ICPEntry = entry; // InteractionContentfulPaints should only happen after a // PerformanceSoftNavigation so the metric should have been set // with a non-zero navigationId mapping to a soft nav. if (!metric.navigationId) continue; // Ignore interactions not for this soft nav // (either paints that have bled into this interaction or paints when // we should have already finalized) if ('interactionId' in ICPEntry && ICPEntry.interactionId != metric.navigationInteractionId) { continue; } renderTime = ICPEntry.largestContentfulPaint?.renderTime || 0; // Paints should never be less than 0 but add cap just in case value = Math.max(renderTime - entry.startTime, 0); if (ICPEntry.largestContentfulPaint) { lcpEntryManager._processEntry(ICPEntry.largestContentfulPaint); metricEntries = [ICPEntry.largestContentfulPaint]; } } // Only report if the page wasn't hidden prior to LCP. if (renderTime < visibilityWatcher.firstHiddenTime) { metric.value = value; metric.entries = metricEntries; report(); } } }; const types = ['largest-contentful-paint']; if (softNavsEnabled) { types.push('interaction-contentful-paint', 'soft-navigation'); } const po = observe(types, handleEntries); if (po) { report = bindReporter(onReport, metric, LCPThresholds, opts.reportAllChanges); const finalizeEventTypes = ['keydown', 'click', 'visibilitychange']; const finalizeLCP = (event) => { if (event.isTrusted && !isFinalized) { // Wrap the listener in an idle callback so it's run in a separate // task to reduce potential INP impact. // https://github.com/GoogleChrome/web-vitals/issues/383 const metricIdToFinalize = metric.id; whenIdleOrHidden(() => { if (!isFinalized) { if (!softNavsEnabled) { // Do some clean up since these won't be needed anymore. po.disconnect(); for (const type of finalizeEventTypes) { removeEventListener(type, finalizeLCP, { capture: true }); } } // As this is in a whenIdleOrHidden check, whether we're still // on the metric you meant to finalize, and ignore if we've moved // on in the meantime. if (metricIdToFinalize === metric.id) { isFinalized = true; report(true); } } }); } }; // Finalize the current LCP after input or visibilitychange. // Although the browser will automatically stop emitting entries in these // cases, we don't know it's finalized, so we track to allow early report. // Note: while scrolling is an input that stops LCP observation, it's // unreliable since it can be programmatically generated. // See: https://github.com/GoogleChrome/web-vitals/issues/75 for (const type of finalizeEventTypes) { addEventListener(type, finalizeLCP, { capture: true, }); } // Only report after a bfcache restore if the `PerformanceObserver` // successfully registered. onBFCacheRestore((event) => { initNewLCPMetric('back-forward-cache', metric.navigationId, metric.navigationInteractionId, metric.navigationURL, getBFCacheRestoreTime()); report = bindReporter(onReport, metric, LCPThresholds, opts.reportAllChanges); doubleRAF(() => { metric.value = performance.now() - event.timeStamp; isFinalized = true; report(true); }); }); } }); }; //# sourceMappingURL=onLCP.js.map