UNPKG

chrome-devtools-frontend

Version:
277 lines (251 loc) • 10.1 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'; import type { LCPMetric, Metric, MetricRatingThresholds, ReportOpts, } from './types.js'; /** Thresholds for LCP. See https://web.dev/articles/lcp#what_is_a_good_lcp_score */ export const LCPThresholds: MetricRatingThresholds = [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: (metric: LCPMetric) => void, opts: ReportOpts = {}, ) => { // 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: ReturnType<typeof bindReporter>; const lcpEntryManager = initUnique(opts, LCPEntryManager); const initNewLCPMetric = ( navigation?: Metric['navigationType'], navigationId?: number, navigationInteractionId?: number, navigationURL?: string, navigationStartTime?: number, ) => { 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: PerformanceSoftNavigation) => { 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: ( | LargestContentfulPaint | InteractionContentfulPaint | PerformanceSoftNavigation )[], ) => { // 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 as PerformanceSoftNavigation); continue; } let value = 0; let metricEntries: LargestContentfulPaint[] = []; 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 as LargestContentfulPaint); metricEntries = [entry as LargestContentfulPaint]; } else if (entry.entryType === 'interaction-contentful-paint') { const ICPEntry = entry as InteractionContentfulPaint; // 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'] as ( | 'largest-contentful-paint' | 'interaction-contentful-paint' | 'soft-navigation' )[]; 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: 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); }); }); } }); };