Last active
November 2, 2020 09:44
-
-
Save saitonakamura/f2c230f9a87bb6602ef4ddc10a1780d9 to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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