Skip to content

Commit

Permalink
Add a metric rating property
Browse files Browse the repository at this point in the history
  • Loading branch information
philipwalton committed Jul 21, 2022
1 parent cbaa843 commit c238ada
Show file tree
Hide file tree
Showing 17 changed files with 236 additions and 47 deletions.
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 'ni';
}
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' | 'ni' | '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 c238ada

Please sign in to comment.