UNPKG

4.42 kBPlain TextView Raw
1/*
2 * Copyright 2020 Google LLC
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 * https://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17import {onBFCacheRestore} from './lib/bfcache.js';
18import {bindReporter} from './lib/bindReporter.js';
19import {doubleRAF} from './lib/doubleRAF.js';
20import {getActivationStart} from './lib/getActivationStart.js';
21import {getVisibilityWatcher} from './lib/getVisibilityWatcher.js';
22import {initMetric} from './lib/initMetric.js';
23import {observe} from './lib/observe.js';
24import {onHidden} from './lib/onHidden.js';
25import {runOnce} from './lib/runOnce.js';
26import {whenActivated} from './lib/whenActivated.js';
27import {LCPMetric, ReportCallback, ReportOpts} from './types.js';
28
29const reportedMetricIDs: Record<string, boolean> = {};
30
31/**
32 * Calculates the [LCP](https://web.dev/lcp/) value for the current page and
33 * calls the `callback` function once the value is ready (along with the
34 * relevant `largest-contentful-paint` performance entry used to determine the
35 * value). The reported value is a `DOMHighResTimeStamp`.
36 *
37 * If the `reportAllChanges` configuration option is set to `true`, the
38 * `callback` function will be called any time a new `largest-contentful-paint`
39 * performance entry is dispatched, or once the final value of the metric has
40 * been determined.
41 */
42export const onLCP = (onReport: ReportCallback, opts?: ReportOpts) => {
43 // Set defaults
44 opts = opts || {};
45
46 whenActivated(() => {
47 // https://web.dev/lcp/#what-is-a-good-lcp-score
48 const thresholds = [2500, 4000];
49
50 const visibilityWatcher = getVisibilityWatcher();
51 let metric = initMetric('LCP');
52 let report: ReturnType<typeof bindReporter>;
53
54 const handleEntries = (entries: LCPMetric['entries']) => {
55 const lastEntry = entries[entries.length - 1] as LargestContentfulPaint;
56 if (lastEntry) {
57 // The startTime attribute returns the value of the renderTime if it is
58 // not 0, and the value of the loadTime otherwise. The activationStart
59 // reference is used because LCP should be relative to page activation
60 // rather than navigation start if the page was prerendered. But in cases
61 // where `activationStart` occurs after the LCP, this time should be
62 // clamped at 0.
63 const value = Math.max(lastEntry.startTime - getActivationStart(), 0);
64
65 // Only report if the page wasn't hidden prior to LCP.
66 if (value < visibilityWatcher.firstHiddenTime) {
67 metric.value = value;
68 metric.entries = [lastEntry];
69 report();
70 }
71 }
72 };
73
74 const po = observe('largest-contentful-paint', handleEntries);
75
76 if (po) {
77 report = bindReporter(
78 onReport,
79 metric,
80 thresholds,
81 opts!.reportAllChanges
82 );
83
84 const stopListening = runOnce(() => {
85 if (!reportedMetricIDs[metric.id]) {
86 handleEntries(po!.takeRecords() as LCPMetric['entries']);
87 po!.disconnect();
88 reportedMetricIDs[metric.id] = true;
89 report(true);
90 }
91 });
92
93 // Stop listening after input. Note: while scrolling is an input that
94 // stops LCP observation, it's unreliable since it can be programmatically
95 // generated. See: https://github.com/GoogleChrome/web-vitals/issues/75
96 ['keydown', 'click'].forEach((type) => {
97 addEventListener(type, stopListening, true);
98 });
99
100 onHidden(stopListening);
101
102 // Only report after a bfcache restore if the `PerformanceObserver`
103 // successfully registered.
104 onBFCacheRestore((event) => {
105 metric = initMetric('LCP');
106 report = bindReporter(
107 onReport,
108 metric,
109 thresholds,
110 opts!.reportAllChanges
111 );
112
113 doubleRAF(() => {
114 metric.value = performance.now() - event.timeStamp;
115 reportedMetricIDs[metric.id] = true;
116 report(true);
117 });
118 });
119 }
120 });
121};