Skip to content

Commit 57bd25f

Browse files
committed
Add a metric rating property
1 parent 9087cad commit 57bd25f

18 files changed

+274
-64
lines changed

README.md

Lines changed: 38 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -421,7 +421,7 @@ function sendToGoogleAnalytics({name, delta, value, id}) {
421421

422422
// OPTIONAL: any additional params or debug info here.
423423
// See: https://web.dev/debug-web-vitals-in-the-field/
424-
// metric_rating: 'good' | 'ni' | 'poor',
424+
// metric_rating: 'good' | 'needs-improvement' | 'poor',
425425
// debug_info: '...',
426426
// ...
427427
});
@@ -657,32 +657,53 @@ The "standard" build of the `web-vitals` library includes some of the same logic
657657

658658
```ts
659659
interface Metric {
660-
// The name of the metric (in acronym form).
660+
/**
661+
* The name of the metric (in acronym form).
662+
*/
661663
name: 'CLS' | 'FCP' | 'FID' | 'INP' | 'LCP' | 'TTFB';
662664

663-
// The current value of the metric.
665+
/**
666+
* The current value of the metric.
667+
*/
664668
value: number;
665669

666-
// The delta between the current value and the last-reported value.
667-
// On the first report, `delta` and `value` will always be the same.
670+
/**
671+
* The rating as to whether the metric value is within the "good",
672+
* "needs improvement", or "poor" thresholds of the metric.
673+
*/
674+
rating: 'good' | 'needs-improvement' | 'poor';
675+
676+
/**
677+
* The delta between the current value and the last-reported value.
678+
* On the first report, `delta` and `value` will always be the same.
679+
*/
668680
delta: number;
669681

670-
// A unique ID representing this particular metric that's specific to the
671-
// current page. This ID can be used by an analytics tool to dedupe
672-
// multiple values sent for the same metric, or to group multiple deltas
673-
// together and calculate a total.
682+
/**
683+
* A unique ID representing this particular metric instance. This ID can
684+
* be used by an analytics tool to dedupe multiple values sent for the same
685+
* metric instance, or to group multiple deltas together and calculate a
686+
* total. It can also be used to differentiate multiple different metric
687+
* instances sent from the same page, which can happen if the page is
688+
* restored from the back/forward cache (in that case new metrics object
689+
* get created).
690+
*/
674691
id: string;
675692

676-
// Any performance entries relevant to the metric value calculation.
677-
// The array may also be empty if the metric value was not based on any
678-
// entries (e.g. a CLS value of 0 given no layout shifts).
693+
/**
694+
* Any performance entries relevant to the metric value calculation.
695+
* The array may also be empty if the metric value was not based on any
696+
* entries (e.g. a CLS value of 0 given no layout shifts).
697+
*/
679698
entries: (PerformanceEntry | LayoutShift | FirstInputPolyfillEntry | NavigationTimingPolyfillEntry)[];
680699

681-
// For regular navigations, the type will be the same as the type indicated
682-
// by the Navigation Timing API (or `undefined` if the browser doesn't
683-
// support that API). For pages that are restored from the bfcache, this
684-
// value will be 'back_forward_cache'.
685-
navigationType: NavigationType | 'back_forward_cache' | 'prerender' | undefined;
700+
/**
701+
* For regular navigations, the type will be the same as the type indicated
702+
* by the Navigation Timing API (or `undefined` if the browser doesn't
703+
* support that API). For pages that are restored from the bfcache, this
704+
* value will be 'back_forward_cache'.
705+
*/
706+
navigationType: NavigationTimingType | 'back_forward_cache' | 'prerender' | undefined;
686707
}
687708
```
688709

src/lib/bindReporter.ts

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,23 +17,38 @@
1717
import {Metric, ReportCallback} from '../types.js';
1818

1919

20+
const getRating = (value: number, thresholds: number[]) => {
21+
if (value > thresholds[1]) {
22+
return 'poor';
23+
}
24+
if (value > thresholds[0]) {
25+
return 'needs-improvement';
26+
}
27+
return 'good';
28+
};
29+
30+
2031
export const bindReporter = (
2132
callback: ReportCallback,
2233
metric: Metric,
34+
thresholds: number[],
2335
reportAllChanges?: boolean,
2436
) => {
2537
let prevValue: number;
38+
let delta: number;
2639
return (forceReport?: boolean) => {
2740
if (metric.value >= 0) {
2841
if (forceReport || reportAllChanges) {
29-
metric.delta = metric.value - (prevValue || 0);
42+
delta = metric.value - (prevValue || 0);
3043

3144
// Report the metric if there's a non-zero delta or if no previous
3245
// value exists (which can happen in the case of the document becoming
3346
// hidden when the metric value is 0).
3447
// See: https://github.com/GoogleChrome/web-vitals/issues/14
35-
if (metric.delta || prevValue === undefined) {
48+
if (delta || prevValue === undefined) {
3649
prevValue = metric.value;
50+
metric.delta = delta;
51+
metric.rating = getRating(metric.value, thresholds);
3752
callback(metric);
3853
}
3954
}

src/lib/initMetric.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ export const initMetric = (name: Metric['name'], value?: number): Metric => {
3838
return {
3939
name,
4040
value: typeof value === 'undefined' ? -1 : value,
41+
rating: 'good', // Will be updated if the value changes.
4142
delta: 0,
4243
entries: [],
4344
id: generateUniqueID(),

src/onCLS.ts

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,9 @@ export const onCLS = (onReport: CLSReportCallback, opts?: ReportOpts) => {
5151
// Set defaults
5252
opts = opts || {};
5353

54+
// https://web.dev/cls/#what-is-a-good-cls-score
55+
const thresholds = [0.1, 0.25];
56+
5457
// Start monitoring FCP so we can only report CLS if FCP is also reported.
5558
// Note: this is done to match the current behavior of CrUX.
5659
if (!isMonitoringFCP) {
@@ -106,7 +109,8 @@ export const onCLS = (onReport: CLSReportCallback, opts?: ReportOpts) => {
106109

107110
const po = observe('layout-shift', handleEntries);
108111
if (po) {
109-
report = bindReporter(onReportWrapped, metric, opts.reportAllChanges);
112+
report = bindReporter(
113+
onReportWrapped, metric, thresholds, opts.reportAllChanges);
110114

111115
onHidden(() => {
112116
handleEntries(po.takeRecords() as CLSMetric['entries']);
@@ -117,7 +121,8 @@ export const onCLS = (onReport: CLSReportCallback, opts?: ReportOpts) => {
117121
sessionValue = 0;
118122
fcpValue = -1;
119123
metric = initMetric('CLS', 0);
120-
report = bindReporter(onReportWrapped, metric, opts!.reportAllChanges);
124+
report = bindReporter(
125+
onReportWrapped, metric, thresholds, opts!.reportAllChanges);
121126
});
122127
}
123128
};

src/onFCP.ts

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,9 @@ export const onFCP = (onReport: FCPReportCallback, opts?: ReportOpts) => {
3232
// Set defaults
3333
opts = opts || {};
3434

35+
// https://web.dev/fcp/#what-is-a-good-fcp-score
36+
const thresholds = [1800, 3000];
37+
3538
const visibilityWatcher = getVisibilityWatcher();
3639
let metric = initMetric('FCP');
3740
let report: ReturnType<typeof bindReporter>;
@@ -68,15 +71,17 @@ export const onFCP = (onReport: FCPReportCallback, opts?: ReportOpts) => {
6871
const po = fcpEntry ? null : observe('paint', handleEntries);
6972

7073
if (fcpEntry || po) {
71-
report = bindReporter(onReport, metric, opts.reportAllChanges);
74+
report = bindReporter(onReport, metric, thresholds, opts.reportAllChanges);
7275

7376
if (fcpEntry) {
7477
handleEntries([fcpEntry]);
7578
}
7679

7780
onBFCacheRestore((event) => {
7881
metric = initMetric('FCP');
79-
report = bindReporter(onReport, metric, opts!.reportAllChanges);
82+
report = bindReporter(
83+
onReport, metric, thresholds, opts!.reportAllChanges);
84+
8085
requestAnimationFrame(() => {
8186
requestAnimationFrame(() => {
8287
metric.value = performance.now() - event.timeStamp;

src/onFID.ts

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,9 @@ export const onFID = (onReport: ReportCallback, opts?: ReportOpts) => {
3636
// Set defaults
3737
opts = opts || {};
3838

39+
// https://web.dev/fid/#what-is-a-good-fid-score
40+
const thresholds = [100, 300];
41+
3942
const visibilityWatcher = getVisibilityWatcher();
4043
let metric = initMetric('FID');
4144
let report: ReturnType<typeof bindReporter>;
@@ -54,7 +57,7 @@ export const onFID = (onReport: ReportCallback, opts?: ReportOpts) => {
5457
}
5558

5659
const po = observe('first-input', handleEntries);
57-
report = bindReporter(onReport, metric, opts.reportAllChanges);
60+
report = bindReporter(onReport, metric, thresholds, opts.reportAllChanges);
5861

5962
if (po) {
6063
onHidden(() => {
@@ -72,7 +75,9 @@ export const onFID = (onReport: ReportCallback, opts?: ReportOpts) => {
7275
}
7376
onBFCacheRestore(() => {
7477
metric = initMetric('FID');
75-
report = bindReporter(onReport, metric, opts!.reportAllChanges);
78+
report = bindReporter(
79+
onReport, metric, thresholds, opts!.reportAllChanges);
80+
7681
window.webVitals.resetFirstInputPolyfill();
7782
window.webVitals.firstInputPolyfill(handleEntry as FirstInputPolyfillCallback);
7883
});
@@ -81,7 +86,9 @@ export const onFID = (onReport: ReportCallback, opts?: ReportOpts) => {
8186
if (po) {
8287
onBFCacheRestore(() => {
8388
metric = initMetric('FID');
84-
report = bindReporter(onReport, metric, opts!.reportAllChanges);
89+
report = bindReporter(
90+
onReport, metric, thresholds, opts!.reportAllChanges);
91+
8592
resetFirstInputPolyfill();
8693
firstInputPolyfill(handleEntry as FirstInputPolyfillCallback);
8794
});

src/onINP.ts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -135,6 +135,9 @@ export const onINP = (onReport: ReportCallback, opts?: ReportOpts) => {
135135
// Set defaults
136136
opts = opts || {};
137137

138+
// https://web.dev/inp/#what's-a-%22good%22-inp-value
139+
const thresholds = [200, 500];
140+
138141
// TODO(philipwalton): remove once the polyfill is no longer needed.
139142
initInteractionCountPolyfill();
140143

@@ -187,7 +190,7 @@ export const onINP = (onReport: ReportCallback, opts?: ReportOpts) => {
187190
durationThreshold: opts.durationThreshold || 40,
188191
} as PerformanceObserverInit);
189192

190-
report = bindReporter(onReport, metric, opts.reportAllChanges);
193+
report = bindReporter(onReport, metric, thresholds, opts.reportAllChanges);
191194

192195
if (po) {
193196
// Also observe entries of type `first-input`. This is useful in cases
@@ -214,7 +217,8 @@ export const onINP = (onReport: ReportCallback, opts?: ReportOpts) => {
214217
prevInteractionCount = getInteractionCount();
215218

216219
metric = initMetric('INP');
217-
report = bindReporter(onReport, metric, opts!.reportAllChanges);
220+
report = bindReporter(
221+
onReport, metric, thresholds, opts!.reportAllChanges);
218222
});
219223
}
220224
};

src/onLCP.ts

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,9 @@ export const onLCP = (onReport: ReportCallback, opts?: ReportOpts) => {
4141
// Set defaults
4242
opts = opts || {};
4343

44+
// https://web.dev/lcp/#what-is-a-good-lcp-score
45+
const thresholds = [2500, 4000];
46+
4447
const visibilityWatcher = getVisibilityWatcher();
4548
let metric = initMetric('LCP');
4649
let report: ReturnType<typeof bindReporter>;
@@ -66,7 +69,7 @@ export const onLCP = (onReport: ReportCallback, opts?: ReportOpts) => {
6669
const po = observe('largest-contentful-paint', handleEntries);
6770

6871
if (po) {
69-
report = bindReporter(onReport, metric, opts.reportAllChanges);
72+
report = bindReporter(onReport, metric, thresholds, opts.reportAllChanges);
7073

7174
const stopListening = () => {
7275
if (!reportedMetricIDs[metric.id]) {
@@ -88,7 +91,9 @@ export const onLCP = (onReport: ReportCallback, opts?: ReportOpts) => {
8891

8992
onBFCacheRestore((event) => {
9093
metric = initMetric('LCP');
91-
report = bindReporter(onReport, metric, opts!.reportAllChanges);
94+
report = bindReporter(
95+
onReport, metric, thresholds, opts!.reportAllChanges);
96+
9297
requestAnimationFrame(() => {
9398
requestAnimationFrame(() => {
9499
metric.value = performance.now() - event.timeStamp;

src/onTTFB.ts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -56,8 +56,12 @@ export const onTTFB = (onReport: ReportCallback, opts?: ReportOpts) => {
5656
// Set defaults
5757
opts = opts || {};
5858

59+
// https://web.dev/ttfb/#what-is-a-good-ttfb-score
60+
const thresholds = [800, 1800];
61+
5962
let metric = initMetric('TTFB');
60-
let report = bindReporter(onReport, metric, opts.reportAllChanges);
63+
let report = bindReporter(
64+
onReport, metric, thresholds, opts.reportAllChanges);
6165

6266
whenReady(() => {
6367
const navEntry = getNavigationEntry();
@@ -83,7 +87,7 @@ export const onTTFB = (onReport: ReportCallback, opts?: ReportOpts) => {
8387

8488
onBFCacheRestore(() => {
8589
metric = initMetric('TTFB', 0);
86-
report = bindReporter(onReport, metric, opts!.reportAllChanges);
90+
report = bindReporter(onReport, metric, thresholds, opts!.reportAllChanges);
8791
report(true);
8892
});
8993
};

src/types/base.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,12 @@ export interface Metric {
2828
*/
2929
value: number;
3030

31+
/**
32+
* The rating as to whether the metric value is within the "good",
33+
* "needs improvement", or "poor" thresholds of the metric.
34+
*/
35+
rating: 'good' | 'needs-improvement' | 'poor';
36+
3137
/**
3238
* The delta between the current value and the last-reported value.
3339
* On the first report, `delta` and `value` will always be the same.

test/css/styles.css

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
body {
2+
background-color: yellow;
3+
}

0 commit comments

Comments
 (0)