expand testing

This commit is contained in:
Michael Freno
2026-01-04 21:22:02 -05:00
parent c6305d2f07
commit 8513651a2e

View File

@@ -9,6 +9,7 @@
* - Total Blocking Time (TBT)
* - Cumulative Layout Shift (CLS)
* - First Input Delay (FID)
* - Interaction to Next Paint (INP)
* - Network requests and bundle sizes
* - JavaScript execution time
*/
@@ -26,6 +27,7 @@ interface PerformanceMetrics {
lcp: number;
cls: number;
fid: number;
inp: number;
ttfb: number;
domContentLoaded: number;
loadComplete: number;
@@ -72,7 +74,9 @@ const TEST_PAGES: PageTestConfig[] = [
{ name: "Blog Post with large banner", path: "/blog/My_MacOS_rice." },
{ name: "Blog Post with Mermaid", path: "/blog/A_Journey_in_Self_Hosting" },
{ name: "Resume", path: "/resume" },
{ name: "Contact", path: "/contact" }
{ name: "Contact", path: "/contact" },
{ name: "Login", path: "/login" },
{ name: "404", path: "/404" }
];
// Add additional blog post path if provided
@@ -89,11 +93,14 @@ async function setupPerformanceObservers(page: Page) {
lcp: 0,
cls: 0,
fid: 0,
inp: 0,
largestContentfulPaint: 0,
cumulativeLayoutShift: 0,
firstInputDelay: 0,
interactionToNextPaint: 0,
layoutShifts: [] as number[],
longTasks: [] as number[]
longTasks: [] as number[],
interactions: [] as number[]
};
// Observe LCP
@@ -162,6 +169,34 @@ async function setupPerformanceObservers(page: Page) {
} catch (e) {
// Long tasks not supported
}
// Observe INP (event timing for interactions)
try {
const inpObserver = new PerformanceObserver((entryList) => {
for (const entry of entryList.getEntries()) {
const eventEntry = entry as any;
if (eventEntry.interactionId) {
const interactionLatency = eventEntry.duration;
(window as any).__perfMetrics.interactions.push(
interactionLatency
);
// INP is the worst (98th percentile) interaction latency
const sortedInteractions = [
...(window as any).__perfMetrics.interactions
].sort((a: number, b: number) => b - a);
const p98Index = Math.floor(sortedInteractions.length * 0.02);
(window as any).__perfMetrics.inp =
sortedInteractions[p98Index] || sortedInteractions[0] || 0;
(window as any).__perfMetrics.interactionToNextPaint = (
window as any
).__perfMetrics.inp;
}
}
});
inpObserver.observe({ type: "event", buffered: true });
} catch (e) {
// Event timing not supported
}
}
});
}
@@ -193,13 +228,16 @@ async function collectPerformanceMetrics(
lcp: 0,
cls: 0,
fid: 0,
longTasks: []
inp: 0,
longTasks: [],
interactions: []
};
// Fallback to direct API if observers didn't capture anything
let lcp = observedMetrics.lcp;
let cls = observedMetrics.cls;
let fid = observedMetrics.fid;
let inp = observedMetrics.inp;
if (lcp === 0) {
const lcpEntries = performance.getEntriesByType(
@@ -221,6 +259,23 @@ async function collectPerformanceMetrics(
.reduce((sum: number, entry: any) => sum + entry.value, 0);
}
// Calculate INP from event timing entries if not already captured
if (inp === 0) {
const eventEntries = performance.getEntriesByType("event") as any[];
const interactionLatencies = eventEntries
.filter((entry: any) => entry.interactionId)
.map((entry: any) => entry.duration);
if (interactionLatencies.length > 0) {
// INP is the 98th percentile of interaction latencies
const sorted = interactionLatencies.sort(
(a: number, b: number) => b - a
);
const p98Index = Math.floor(sorted.length * 0.02);
inp = sorted[p98Index] || sorted[0];
}
}
// Get resource timing
const resources = performance.getEntriesByType(
"resource"
@@ -310,6 +365,7 @@ async function collectPerformanceMetrics(
lcp,
cls,
fid,
inp,
ttfb: perf.responseStart - perf.requestStart,
domContentLoaded: perf.domContentLoadedEventEnd - perf.fetchStart,
loadComplete: perf.loadEventEnd - perf.fetchStart,
@@ -470,14 +526,15 @@ function formatTime(ms: number): string {
}
function getWebVitalRating(
metric: "lcp" | "fcp" | "cls" | "fid",
metric: "lcp" | "fcp" | "cls" | "fid" | "inp",
value: number
): string {
const thresholds = {
lcp: { good: 2500, needsImprovement: 4000 },
fcp: { good: 1800, needsImprovement: 3000 },
cls: { good: 0.1, needsImprovement: 0.25 },
fid: { good: 100, needsImprovement: 300 }
fid: { good: 100, needsImprovement: 300 },
inp: { good: 200, needsImprovement: 500 }
};
const t = thresholds[metric];
@@ -518,6 +575,9 @@ function printResults(results: TestResult[]) {
console.log(
` CLS (Cumulative Layout Shift): ${result.median.cls.toFixed(3).padEnd(8)} | ${result.min.cls.toFixed(3)}${result.max.cls.toFixed(3)} ${getWebVitalRating("cls", result.median.cls)}`
);
console.log(
` INP (Interaction to Next Paint): ${formatTime(result.median.inp).padEnd(8)} | ${formatTime(result.min.inp)}${formatTime(result.max.inp)} ${getWebVitalRating("inp", result.median.inp)}`
);
console.log("\n Loading Metrics (Median):");
console.log(
@@ -594,6 +654,7 @@ function printResults(results: TestResult[]) {
lcp: results.reduce((sum, r) => sum + r.median.lcp, 0) / results.length,
fcp: results.reduce((sum, r) => sum + r.median.fcp, 0) / results.length,
cls: results.reduce((sum, r) => sum + r.median.cls, 0) / results.length,
inp: results.reduce((sum, r) => sum + r.median.inp, 0) / results.length,
ttfb: results.reduce((sum, r) => sum + r.median.ttfb, 0) / results.length,
totalBytes:
results.reduce((sum, r) => sum + r.median.totalBytes, 0) / results.length,
@@ -608,6 +669,7 @@ function printResults(results: TestResult[]) {
console.log(` LCP: ${formatTime(overallAverage.lcp)}`);
console.log(` FCP: ${formatTime(overallAverage.fcp)}`);
console.log(` CLS: ${overallAverage.cls.toFixed(3)}`);
console.log(` INP: ${formatTime(overallAverage.inp)}`);
console.log(` TTFB: ${formatTime(overallAverage.ttfb)}`);
console.log(
` Total Size: ${formatBytes(overallAverage.totalBytes)}`
@@ -662,6 +724,14 @@ function printResults(results: TestResult[]) {
);
}
// Find pages with high INP
const highINP = results.filter((r) => r.median.inp > 200);
if (highINP.length > 0) {
console.log(
`${highINP.length} page(s) with INP > 200ms - optimize event handlers and reduce long tasks`
);
}
console.log("\n");
}