Building a Frontend Performance Monitoring SDK: Theory, Metrics, and Implementation
This article explains the importance of frontend performance monitoring, outlines core metrics such as FCP, LCP, FP, CLS, and demonstrates how to implement a comprehensive SDK using PerformanceObserver, custom configuration, and wrappers for fetch and XMLHttpRequest to capture resource and network data for batch reporting.
In modern frontend development, performance monitoring has become essential for detecting issues, analyzing user behavior, and supporting business decisions. The author introduces a series of articles that combine theory with practice to build a complete frontend monitoring platform.
Core Metrics : The article lists key performance indicators—resource loading details, FCP, LCP, FP, LOAD, FMP, CLS, FID, and TTI—explaining their meanings and calculation methods.
Data Collection Methods : Two approaches are discussed: the modern PerformanceObserver API, which captures performance entries in real time, and the legacy PerformanceTiming API, which is now deprecated. The author recommends using PerformanceObserver exclusively.
Global Configuration :
export const config = {
url: 'http://127.0.0.1:3000/api/data', // reporting endpoint
projectName: 'monitor',
appId: '123456',
userId: '123456',
isAjax: false,
batchSize: 5,
containerElements: ['html', 'body', '#app', '#root'],
skeletonElements: [],
reportBefore: () => {},
reportAfter: () => {},
reportSuccess: () => {},
reportFail: () => {}
};Resource Data Type Definition :
type commonType = {
type: string;
subType: string;
timestamp: number;
};
export type PerformanceResourceType = commonType & {
name: string;
dns: number;
duration: number;
protocol: string;
redirect: number;
resourceSize: number;
responseBodySize: number;
responseHeaderSize: number;
sourceType: string;
startTime: number;
subType: string;
tcp: number;
transferSize: number;
ttfb: number;
pageUrl: string;
};Resource Observer Implementation captures each resource entry, filters out SDK‑generated requests, constructs a PerformanceResourceType object, batches the data, and reports it via lazyReportBatch :
export function observerEvent() {
const config = getConfig();
const url = config.url;
const host = new URL(url).host;
const entryHandler = (list) => {
const dataList = [];
for (const entry of list.getEntries()) {
const resourceEntry = entry;
if (resourceEntry.name.includes(host)) continue;
const data = {
type: TraceTypeEnum.performance,
subType: resourceEntry.entryType,
name: resourceEntry.name,
sourceType: resourceEntry.initiatorType,
duration: resourceEntry.duration,
dns: resourceEntry.domainLookupEnd - resourceEntry.domainLookupStart,
tcp: resourceEntry.connectEnd - resourceEntry.connectStart,
redirect: resourceEntry.redirectEnd - resourceEntry.redirectStart,
ttfb: resourceEntry.responseStart,
protocol: resourceEntry.nextHopProtocol,
responseBodySize: resourceEntry.encodedBodySize,
responseHeaderSize: resourceEntry.transferSize - resourceEntry.encodedBodySize,
transferSize: resourceEntry.transferSize,
resourceSize: resourceEntry.decodedBodySize,
startTime: resourceEntry.startTime,
pageUrl: window.location.href,
timestamp: Date.now()
};
dataList.push(data);
}
lazyReportBatch({
type: TraceTypeEnum.performance,
subType: TraceSubTypeEnum.resource,
resourceList: dataList,
timestamp: Date.now()
});
};
const observer = new PerformanceObserver(entryHandler);
observer.observe({ type: 'resource', buffered: true });
}Page Load Timing records the time between navigation start and the load event, reporting the duration as a performance entry.
export default function observePageLoadTime() {
const startTimestamp = performance.now();
window.addEventListener('load', () => {
const loadTimestamp = performance.now();
const loadTime = loadTimestamp - startTimestamp;
lazyReportBatch({
type: TraceTypeEnum.performance,
subType: TraceSubTypeEnum.load,
entryType: 'load',
pageUrl: window.location.href,
startTime: startTimestamp,
duration: loadTime,
timestamp: Date.now()
});
});
}Paint Metrics (FCP, LCP, FP) are captured with separate observers that listen for paint and largest-contentful-paint entries, convert them to JSON, add metadata, and batch‑report them.
Network Request Monitoring overrides the native fetch function and the XMLHttpRequest prototype to record request URL, method, timing, status, and parameters, then reports the data using the same batch mechanism.
// fetch wrapper example
const originalFetch = window.fetch;
window.fetch = function newFetch(url, config) {
const startTime = Date.now();
const report = { type: TraceTypeEnum.performance, subType: TraceSubTypeEnum.fetch, url: typeof url === 'string' ? url : url.href, method: config?.method || 'GET', startTime, pageUrl: location.href, timestamp: Date.now() };
return originalFetch(url, config)
.then(res => { report.status = res.status; return res; })
.catch(err => { report.status = err.status; throw err; })
.finally(() => {
report.endTime = Date.now();
report.duration = report.endTime - startTime;
report.success = report.status >= 200 && report.status < 300;
lazyReportBatch(report);
});
};The article concludes by noting that the performance monitoring portion is complete and invites readers to provide feedback or report errors, with a promise to cover error monitoring in the next installment.
Rare Earth Juejin Tech Community
Juejin, a tech community that helps developers grow.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.