Add Web Vitals performance tracking (LCP, INP, CLS, FCP, TTFB)
End-to-end performance metrics tracking: dedicated database table with percentile aggregation, tracker collection behind data-perf attribute, /api/send handling, report API endpoints, and Performance dashboard page with threshold-based metric cards, time-series chart, and per-page breakdown. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
1 parent
8cd3c03702
commit
ce9e2416fb
24 files changed
+1028
-8
No files matched your search
@@ -35,6 +35,7 @@
|
||||
const excludeHash = attr(`${_data}exclude-hash`) === _true;
|
||||
const domain = attr(`${_data}domains`) || '';
|
||||
const credentials = attr(`${_data}fetch-credentials`) || 'omit';
|
||||
const perf = attr(`${_data}perf`) === _true;
|
||||
|
||||
const domains = domain.split(',').map(n => n.trim());
|
||||
const host =
|
||||
@@ -194,6 +195,7 @@
|
||||
track();
|
||||
handlePathChanges();
|
||||
handleClicks();
|
||||
if (perf) initPerformance();
|
||||
}
|
||||
};
|
||||
|
||||
@@ -219,6 +221,77 @@
|
||||
);
|
||||
};
|
||||
|
||||
/* Performance */
|
||||
|
||||
const initPerformance = () => {
|
||||
const metrics = {};
|
||||
let sent = false;
|
||||
|
||||
const observe = (type, callback) => {
|
||||
try {
|
||||
const observer = new PerformanceObserver(list => {
|
||||
list.getEntries().forEach(callback);
|
||||
});
|
||||
observer.observe({ type, buffered: true });
|
||||
} catch {
|
||||
/* not supported */
|
||||
}
|
||||
};
|
||||
|
||||
// TTFB
|
||||
observe('navigation', entry => {
|
||||
metrics.ttfb = Math.max(entry.responseStart - entry.requestStart, 0);
|
||||
});
|
||||
|
||||
// FCP
|
||||
observe('paint', entry => {
|
||||
if (entry.name === 'first-contentful-paint') {
|
||||
metrics.fcp = entry.startTime;
|
||||
}
|
||||
});
|
||||
|
||||
// LCP
|
||||
observe('largest-contentful-paint', entry => {
|
||||
metrics.lcp = entry.startTime;
|
||||
});
|
||||
|
||||
// CLS
|
||||
let clsValue = 0;
|
||||
observe('layout-shift', entry => {
|
||||
if (!entry.hadRecentInput) {
|
||||
clsValue += entry.value;
|
||||
metrics.cls = clsValue;
|
||||
}
|
||||
});
|
||||
|
||||
// INP
|
||||
let inpValue = 0;
|
||||
try {
|
||||
const observer = new PerformanceObserver(list => {
|
||||
list.getEntries().forEach(entry => {
|
||||
if (entry.duration > inpValue) {
|
||||
inpValue = entry.duration;
|
||||
metrics.inp = inpValue;
|
||||
}
|
||||
});
|
||||
});
|
||||
observer.observe({ type: 'event', buffered: true, durationThreshold: 16 });
|
||||
} catch {
|
||||
/* not supported */
|
||||
}
|
||||
|
||||
const sendPerformance = () => {
|
||||
if (sent || !Object.keys(metrics).length) return;
|
||||
sent = true;
|
||||
send({ ...getPayload(), ...metrics }, 'performance');
|
||||
};
|
||||
|
||||
document.addEventListener('visibilitychange', () => {
|
||||
if (document.visibilityState === 'hidden') sendPerformance();
|
||||
});
|
||||
window.addEventListener('pagehide', sendPerformance);
|
||||
};
|
||||
|
||||
/* Start */
|
||||
|
||||
if (!window.umami) {
|
||||
|
||||
Reference in new issue
Block a user