Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add a metric rating property #246

Merged
merged 1 commit into from Jul 22, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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;
}