Skip to content

Commit

Permalink
Merge pull request #246 from GoogleChrome/thresholds
Browse files Browse the repository at this point in the history
Add a metric rating property
  • Loading branch information
philipwalton committed Jul 22, 2022
2 parents 9087cad + 57bd25f commit 693e898
Show file tree
Hide file tree
Showing 18 changed files with 274 additions and 64 deletions.
55 changes: 38 additions & 17 deletions README.md
Expand Up @@ -421,7 +421,7 @@ function sendToGoogleAnalytics({name, delta, value, id}) {

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

```ts
interface Metric {
// The name of the metric (in acronym form).
/**
* The name of the metric (in acronym form).
*/
name: 'CLS' | 'FCP' | 'FID' | 'INP' | 'LCP' | 'TTFB';

// The current value of the metric.
/**
* The current value of the metric.
*/
value: number;

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

/**
* The delta between the current value and the last-reported value.
* On the first report, `delta` and `value` will always be the same.
*/
delta: number;

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

// Any performance entries relevant to the metric value calculation.
// The array may also be empty if the metric value was not based on any
// entries (e.g. a CLS value of 0 given no layout shifts).
/**
* Any performance entries relevant to the metric value calculation.
* The array may also be empty if the metric value was not based on any
* entries (e.g. a CLS value of 0 given no layout shifts).
*/
entries: (PerformanceEntry | LayoutShift | FirstInputPolyfillEntry | NavigationTimingPolyfillEntry)[];

// For regular navigations, the type will be the same as the type indicated
// by the Navigation Timing API (or `undefined` if the browser doesn't
// support that API). For pages that are restored from the bfcache, this
// value will be 'back_forward_cache'.
navigationType: NavigationType | 'back_forward_cache' | 'prerender' | undefined;
/**
* For regular navigations, the type will be the same as the type indicated
* by the Navigation Timing API (or `undefined` if the browser doesn't
* support that API). For pages that are restored from the bfcache, this
* value will be 'back_forward_cache'.
*/
navigationType: NavigationTimingType | 'back_forward_cache' | 'prerender' | undefined;
}
```

Expand Down
19 changes: 17 additions & 2 deletions src/lib/bindReporter.ts
Expand Up @@ -17,23 +17,38 @@
import {Metric, ReportCallback} from '../types.js';


const getRating = (value: number, thresholds: number[]) => {
if (value > thresholds[1]) {
return 'poor';
}
if (value > thresholds[0]) {
return 'needs-improvement';
}
return 'good';
};


export const bindReporter = (
callback: ReportCallback,
metric: Metric,
thresholds: number[],
reportAllChanges?: boolean,
) => {
let prevValue: number;
let delta: number;
return (forceReport?: boolean) => {
if (metric.value >= 0) {
if (forceReport || reportAllChanges) {
metric.delta = metric.value - (prevValue || 0);
delta = metric.value - (prevValue || 0);

// Report the metric if there's a non-zero delta or if no previous
// value exists (which can happen in the case of the document becoming
// hidden when the metric value is 0).
// See: https://github.com/GoogleChrome/web-vitals/issues/14
if (metric.delta || prevValue === undefined) {
if (delta || prevValue === undefined) {
prevValue = metric.value;
metric.delta = delta;
metric.rating = getRating(metric.value, thresholds);
callback(metric);
}
}
Expand Down
1 change: 1 addition & 0 deletions src/lib/initMetric.ts
Expand Up @@ -38,6 +38,7 @@ export const initMetric = (name: Metric['name'], value?: number): Metric => {
return {
name,
value: typeof value === 'undefined' ? -1 : value,
rating: 'good', // Will be updated if the value changes.
delta: 0,
entries: [],
id: generateUniqueID(),
Expand Down
9 changes: 7 additions & 2 deletions src/onCLS.ts
Expand Up @@ -51,6 +51,9 @@ export const onCLS = (onReport: CLSReportCallback, opts?: ReportOpts) => {
// Set defaults
opts = opts || {};

// https://web.dev/cls/#what-is-a-good-cls-score
const thresholds = [0.1, 0.25];

// Start monitoring FCP so we can only report CLS if FCP is also reported.
// Note: this is done to match the current behavior of CrUX.
if (!isMonitoringFCP) {
Expand Down Expand Up @@ -106,7 +109,8 @@ export const onCLS = (onReport: CLSReportCallback, opts?: ReportOpts) => {

const po = observe('layout-shift', handleEntries);
if (po) {
report = bindReporter(onReportWrapped, metric, opts.reportAllChanges);
report = bindReporter(
onReportWrapped, metric, thresholds, opts.reportAllChanges);

onHidden(() => {
handleEntries(po.takeRecords() as CLSMetric['entries']);
Expand All @@ -117,7 +121,8 @@ export const onCLS = (onReport: CLSReportCallback, opts?: ReportOpts) => {
sessionValue = 0;
fcpValue = -1;
metric = initMetric('CLS', 0);
report = bindReporter(onReportWrapped, metric, opts!.reportAllChanges);
report = bindReporter(
onReportWrapped, metric, thresholds, opts!.reportAllChanges);
});
}
};
9 changes: 7 additions & 2 deletions src/onFCP.ts
Expand Up @@ -32,6 +32,9 @@ export const onFCP = (onReport: FCPReportCallback, opts?: ReportOpts) => {
// Set defaults
opts = opts || {};

// https://web.dev/fcp/#what-is-a-good-fcp-score
const thresholds = [1800, 3000];

const visibilityWatcher = getVisibilityWatcher();
let metric = initMetric('FCP');
let report: ReturnType<typeof bindReporter>;
Expand Down Expand Up @@ -68,15 +71,17 @@ export const onFCP = (onReport: FCPReportCallback, opts?: ReportOpts) => {
const po = fcpEntry ? null : observe('paint', handleEntries);

if (fcpEntry || po) {
report = bindReporter(onReport, metric, opts.reportAllChanges);
report = bindReporter(onReport, metric, thresholds, opts.reportAllChanges);

if (fcpEntry) {
handleEntries([fcpEntry]);
}

onBFCacheRestore((event) => {
metric = initMetric('FCP');
report = bindReporter(onReport, metric, opts!.reportAllChanges);
report = bindReporter(
onReport, metric, thresholds, opts!.reportAllChanges);

requestAnimationFrame(() => {
requestAnimationFrame(() => {
metric.value = performance.now() - event.timeStamp;
Expand Down
13 changes: 10 additions & 3 deletions src/onFID.ts
Expand Up @@ -36,6 +36,9 @@ export const onFID = (onReport: ReportCallback, opts?: ReportOpts) => {
// Set defaults
opts = opts || {};

// https://web.dev/fid/#what-is-a-good-fid-score
const thresholds = [100, 300];

const visibilityWatcher = getVisibilityWatcher();
let metric = initMetric('FID');
let report: ReturnType<typeof bindReporter>;
Expand All @@ -54,7 +57,7 @@ export const onFID = (onReport: ReportCallback, opts?: ReportOpts) => {
}

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

if (po) {
onHidden(() => {
Expand All @@ -72,7 +75,9 @@ export const onFID = (onReport: ReportCallback, opts?: ReportOpts) => {
}
onBFCacheRestore(() => {
metric = initMetric('FID');
report = bindReporter(onReport, metric, opts!.reportAllChanges);
report = bindReporter(
onReport, metric, thresholds, opts!.reportAllChanges);

window.webVitals.resetFirstInputPolyfill();
window.webVitals.firstInputPolyfill(handleEntry as FirstInputPolyfillCallback);
});
Expand All @@ -81,7 +86,9 @@ export const onFID = (onReport: ReportCallback, opts?: ReportOpts) => {
if (po) {
onBFCacheRestore(() => {
metric = initMetric('FID');
report = bindReporter(onReport, metric, opts!.reportAllChanges);
report = bindReporter(
onReport, metric, thresholds, opts!.reportAllChanges);

resetFirstInputPolyfill();
firstInputPolyfill(handleEntry as FirstInputPolyfillCallback);
});
Expand Down
8 changes: 6 additions & 2 deletions src/onINP.ts
Expand Up @@ -135,6 +135,9 @@ export const onINP = (onReport: ReportCallback, opts?: ReportOpts) => {
// Set defaults
opts = opts || {};

// https://web.dev/inp/#what's-a-%22good%22-inp-value
const thresholds = [200, 500];

// TODO(philipwalton): remove once the polyfill is no longer needed.
initInteractionCountPolyfill();

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

report = bindReporter(onReport, metric, opts.reportAllChanges);
report = bindReporter(onReport, metric, thresholds, opts.reportAllChanges);

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

metric = initMetric('INP');
report = bindReporter(onReport, metric, opts!.reportAllChanges);
report = bindReporter(
onReport, metric, thresholds, opts!.reportAllChanges);
});
}
};
9 changes: 7 additions & 2 deletions src/onLCP.ts
Expand Up @@ -41,6 +41,9 @@ export const onLCP = (onReport: ReportCallback, opts?: ReportOpts) => {
// Set defaults
opts = opts || {};

// https://web.dev/lcp/#what-is-a-good-lcp-score
const thresholds = [2500, 4000];

const visibilityWatcher = getVisibilityWatcher();
let metric = initMetric('LCP');
let report: ReturnType<typeof bindReporter>;
Expand All @@ -66,7 +69,7 @@ export const onLCP = (onReport: ReportCallback, opts?: ReportOpts) => {
const po = observe('largest-contentful-paint', handleEntries);

if (po) {
report = bindReporter(onReport, metric, opts.reportAllChanges);
report = bindReporter(onReport, metric, thresholds, opts.reportAllChanges);

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

onBFCacheRestore((event) => {
metric = initMetric('LCP');
report = bindReporter(onReport, metric, opts!.reportAllChanges);
report = bindReporter(
onReport, metric, thresholds, opts!.reportAllChanges);

requestAnimationFrame(() => {
requestAnimationFrame(() => {
metric.value = performance.now() - event.timeStamp;
Expand Down
8 changes: 6 additions & 2 deletions src/onTTFB.ts
Expand Up @@ -56,8 +56,12 @@ export const onTTFB = (onReport: ReportCallback, opts?: ReportOpts) => {
// Set defaults
opts = opts || {};

// https://web.dev/ttfb/#what-is-a-good-ttfb-score
const thresholds = [800, 1800];

let metric = initMetric('TTFB');
let report = bindReporter(onReport, metric, opts.reportAllChanges);
let report = bindReporter(
onReport, metric, thresholds, opts.reportAllChanges);

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

onBFCacheRestore(() => {
metric = initMetric('TTFB', 0);
report = bindReporter(onReport, metric, opts!.reportAllChanges);
report = bindReporter(onReport, metric, thresholds, opts!.reportAllChanges);
report(true);
});
};
6 changes: 6 additions & 0 deletions src/types/base.ts
Expand Up @@ -28,6 +28,12 @@ export interface Metric {
*/
value: number;

/**
* The rating as to whether the metric value is within the "good",
* "needs improvement", or "poor" thresholds of the metric.
*/
rating: 'good' | 'needs-improvement' | 'poor';

/**
* The delta between the current value and the last-reported value.
* On the first report, `delta` and `value` will always be the same.
Expand Down
3 changes: 3 additions & 0 deletions test/css/styles.css
@@ -0,0 +1,3 @@
body {
background-color: yellow;
}

0 comments on commit 693e898

Please sign in to comment.