Skip to content

Instantly share code, notes, and snippets.

@saitonakamura
Last active November 2, 2020 09:44
Show Gist options
  • Save saitonakamura/f2c230f9a87bb6602ef4ddc10a1780d9 to your computer and use it in GitHub Desktop.
Save saitonakamura/f2c230f9a87bb6602ef4ddc10a1780d9 to your computer and use it in GitHub Desktop.
import { noop, omit } from 'lodash';
import { getCLS, getFCP, getFID, getLCP, getTTFB, ReportHandler } from 'web-vitals'
type IEntryType = 'navigation' | 'resource' | 'paint' | 'measure' | string;
type IAppPerformanceMeasure = 'clientHydration' | 'ssrHydration' | string;
type IAppPerformanceMark =
| 'clientHydrateBegin'
| 'clientHydrateEnd'
| 'ssrHydrateBegin'
| 'ssrHydrateEnd'
| string;
type IPerformanceObservationInputCriterion = {
entryType: IEntryType;
name: IAppPerformanceMeasure | RegExp;
sample?: number;
};
type IPerformanceObservationCriterionValue = Pick<
IPerformanceObservationInputCriterion,
'name' | 'sample'
>;
const metricUrl = 'YOUR_METRIC_REPORT_URL';
const reportPerformanceEntry = (entry: PerformanceEntry) => {
if (!('toJSON' in entry)) {
return Promise.resolve(false);
}
let data = entry.toJSON();
data.value = entry.duration;
data = omit(data, 'duration');
return analyticsSend(metricUrl, { data });
};
const reportWebVital: ReportHandler = (metric) =>
analyticsSend(metricUrl, {
data: {
...metric,
entries: metric.entries.filter((m) => 'toJSON' in m).map((m) => m.toJSON()),
},
});
const forcedObservationsEnabled = localStorage.getItem('isPerformanceObservationsEnabled');
const calculateIsSampled = (samplePercent: number) => {
if (forcedObservationsEnabled) {
return true;
}
return Math.random() < samplePercent;
};
function doIfAvailable(func: (performance: Performance) => void): void;
function doIfAvailable<T>(func: (performance: Performance) => T, fallbackReturnValue: T): T;
function doIfAvailable<T>(
func: (performance: Performance) => T | void,
fallbackReturnValue?: T | undefined,
): T | void {
if ('PerformanceObserver' in window) {
return func(window.performance);
}
return fallbackReturnValue;
}
const filterName = (filter: IPerformanceObservationCriterionValue['name'], name: string) => {
if (typeof filter === 'string') {
if (filter === name) {
return true;
}
}
if (filter instanceof RegExp) {
if (filter.test(name)) {
return true;
}
}
return false;
};
const getCriterionMap = (
criteria: IPerformanceObservationInputCriterion[],
): Map<IEntryType, ReadonlyArray<IPerformanceObservationCriterionValue>> => {
const criterionMap = new Map<IEntryType, IPerformanceObservationCriterionValue[]>();
for (const criterion of criteria) {
const criterionValue: IPerformanceObservationCriterionValue = {
sample: criterion.sample,
name: criterion.name,
};
const criterionValues = criterionMap.get(criterion.entryType);
if (criterionValues) {
criterionValues.push(criterionValue);
} else {
criterionMap.set(criterion.entryType, [criterionValue]);
}
}
return criterionMap;
};
export const startPerformanceObservations = (
criteria: IPerformanceObservationInputCriterion[],
globalSamplePercent: number,
) => {
const globalIsSampled = calculateIsSampled(globalSamplePercent);
const criterionMap = getCriterionMap(criteria);
return doIfAvailable((performance) => {
const filterAndReport = (entryList: PerformanceEntryList) => {
for (const entry of entryList) {
const criterion = criterionMap.get(entry.entryType);
if (!criterion) {
// eslint-disable-next-line no-continue
continue;
}
const satisfiesAndSampled = criterion.some(
({ sample, name }) =>
(sample ? calculateIsSampled(sample) : globalIsSampled) && filterName(name, entry.name),
);
if (!satisfiesAndSampled) {
// eslint-disable-next-line no-continue
continue;
}
reportPerformanceEntry(entry);
}
};
const observer = new PerformanceObserver((entryList) => {
filterAndReport(entryList.getEntries());
});
const entriesBeforeObservations = performance.getEntries();
if (globalIsSampled) {
getTTFB(reportWebVital);
getFCP(reportWebVital);
getLCP(reportWebVital);
getCLS(reportWebVital);
getFID(reportWebVital);
}
filterAndReport(entriesBeforeObservations);
observer.observe({ entryTypes: Array.from(criterionMap.keys()) });
return () => observer.disconnect();
}, noop);
};
export const performanceMark = (mark: IAppPerformanceMark) => {
doIfAvailable((performance) => {
performance.mark(mark);
});
};
export const performanceMarkMeasure = (
mark: IAppPerformanceMark,
measureOptions: { name: IAppPerformanceMeasure; from: IAppPerformanceMark },
) => {
doIfAvailable((performance) => {
performance.mark(mark);
performance.measure(measureOptions.name, measureOptions.from, mark);
});
};
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment