expand testing
This commit is contained in:
@@ -9,6 +9,7 @@
|
|||||||
* - Total Blocking Time (TBT)
|
* - Total Blocking Time (TBT)
|
||||||
* - Cumulative Layout Shift (CLS)
|
* - Cumulative Layout Shift (CLS)
|
||||||
* - First Input Delay (FID)
|
* - First Input Delay (FID)
|
||||||
|
* - Interaction to Next Paint (INP)
|
||||||
* - Network requests and bundle sizes
|
* - Network requests and bundle sizes
|
||||||
* - JavaScript execution time
|
* - JavaScript execution time
|
||||||
*/
|
*/
|
||||||
@@ -26,6 +27,7 @@ interface PerformanceMetrics {
|
|||||||
lcp: number;
|
lcp: number;
|
||||||
cls: number;
|
cls: number;
|
||||||
fid: number;
|
fid: number;
|
||||||
|
inp: number;
|
||||||
ttfb: number;
|
ttfb: number;
|
||||||
domContentLoaded: number;
|
domContentLoaded: number;
|
||||||
loadComplete: 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 large banner", path: "/blog/My_MacOS_rice." },
|
||||||
{ name: "Blog Post with Mermaid", path: "/blog/A_Journey_in_Self_Hosting" },
|
{ name: "Blog Post with Mermaid", path: "/blog/A_Journey_in_Self_Hosting" },
|
||||||
{ name: "Resume", path: "/resume" },
|
{ 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
|
// Add additional blog post path if provided
|
||||||
@@ -89,11 +93,14 @@ async function setupPerformanceObservers(page: Page) {
|
|||||||
lcp: 0,
|
lcp: 0,
|
||||||
cls: 0,
|
cls: 0,
|
||||||
fid: 0,
|
fid: 0,
|
||||||
|
inp: 0,
|
||||||
largestContentfulPaint: 0,
|
largestContentfulPaint: 0,
|
||||||
cumulativeLayoutShift: 0,
|
cumulativeLayoutShift: 0,
|
||||||
firstInputDelay: 0,
|
firstInputDelay: 0,
|
||||||
|
interactionToNextPaint: 0,
|
||||||
layoutShifts: [] as number[],
|
layoutShifts: [] as number[],
|
||||||
longTasks: [] as number[]
|
longTasks: [] as number[],
|
||||||
|
interactions: [] as number[]
|
||||||
};
|
};
|
||||||
|
|
||||||
// Observe LCP
|
// Observe LCP
|
||||||
@@ -162,6 +169,34 @@ async function setupPerformanceObservers(page: Page) {
|
|||||||
} catch (e) {
|
} catch (e) {
|
||||||
// Long tasks not supported
|
// 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,
|
lcp: 0,
|
||||||
cls: 0,
|
cls: 0,
|
||||||
fid: 0,
|
fid: 0,
|
||||||
longTasks: []
|
inp: 0,
|
||||||
|
longTasks: [],
|
||||||
|
interactions: []
|
||||||
};
|
};
|
||||||
|
|
||||||
// Fallback to direct API if observers didn't capture anything
|
// Fallback to direct API if observers didn't capture anything
|
||||||
let lcp = observedMetrics.lcp;
|
let lcp = observedMetrics.lcp;
|
||||||
let cls = observedMetrics.cls;
|
let cls = observedMetrics.cls;
|
||||||
let fid = observedMetrics.fid;
|
let fid = observedMetrics.fid;
|
||||||
|
let inp = observedMetrics.inp;
|
||||||
|
|
||||||
if (lcp === 0) {
|
if (lcp === 0) {
|
||||||
const lcpEntries = performance.getEntriesByType(
|
const lcpEntries = performance.getEntriesByType(
|
||||||
@@ -221,6 +259,23 @@ async function collectPerformanceMetrics(
|
|||||||
.reduce((sum: number, entry: any) => sum + entry.value, 0);
|
.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
|
// Get resource timing
|
||||||
const resources = performance.getEntriesByType(
|
const resources = performance.getEntriesByType(
|
||||||
"resource"
|
"resource"
|
||||||
@@ -310,6 +365,7 @@ async function collectPerformanceMetrics(
|
|||||||
lcp,
|
lcp,
|
||||||
cls,
|
cls,
|
||||||
fid,
|
fid,
|
||||||
|
inp,
|
||||||
ttfb: perf.responseStart - perf.requestStart,
|
ttfb: perf.responseStart - perf.requestStart,
|
||||||
domContentLoaded: perf.domContentLoadedEventEnd - perf.fetchStart,
|
domContentLoaded: perf.domContentLoadedEventEnd - perf.fetchStart,
|
||||||
loadComplete: perf.loadEventEnd - perf.fetchStart,
|
loadComplete: perf.loadEventEnd - perf.fetchStart,
|
||||||
@@ -470,14 +526,15 @@ function formatTime(ms: number): string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function getWebVitalRating(
|
function getWebVitalRating(
|
||||||
metric: "lcp" | "fcp" | "cls" | "fid",
|
metric: "lcp" | "fcp" | "cls" | "fid" | "inp",
|
||||||
value: number
|
value: number
|
||||||
): string {
|
): string {
|
||||||
const thresholds = {
|
const thresholds = {
|
||||||
lcp: { good: 2500, needsImprovement: 4000 },
|
lcp: { good: 2500, needsImprovement: 4000 },
|
||||||
fcp: { good: 1800, needsImprovement: 3000 },
|
fcp: { good: 1800, needsImprovement: 3000 },
|
||||||
cls: { good: 0.1, needsImprovement: 0.25 },
|
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];
|
const t = thresholds[metric];
|
||||||
@@ -518,6 +575,9 @@ function printResults(results: TestResult[]) {
|
|||||||
console.log(
|
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)}`
|
` 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("\n Loading Metrics (Median):");
|
||||||
console.log(
|
console.log(
|
||||||
@@ -594,6 +654,7 @@ function printResults(results: TestResult[]) {
|
|||||||
lcp: results.reduce((sum, r) => sum + r.median.lcp, 0) / results.length,
|
lcp: results.reduce((sum, r) => sum + r.median.lcp, 0) / results.length,
|
||||||
fcp: results.reduce((sum, r) => sum + r.median.fcp, 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,
|
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,
|
ttfb: results.reduce((sum, r) => sum + r.median.ttfb, 0) / results.length,
|
||||||
totalBytes:
|
totalBytes:
|
||||||
results.reduce((sum, r) => sum + r.median.totalBytes, 0) / results.length,
|
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(` LCP: ${formatTime(overallAverage.lcp)}`);
|
||||||
console.log(` FCP: ${formatTime(overallAverage.fcp)}`);
|
console.log(` FCP: ${formatTime(overallAverage.fcp)}`);
|
||||||
console.log(` CLS: ${overallAverage.cls.toFixed(3)}`);
|
console.log(` CLS: ${overallAverage.cls.toFixed(3)}`);
|
||||||
|
console.log(` INP: ${formatTime(overallAverage.inp)}`);
|
||||||
console.log(` TTFB: ${formatTime(overallAverage.ttfb)}`);
|
console.log(` TTFB: ${formatTime(overallAverage.ttfb)}`);
|
||||||
console.log(
|
console.log(
|
||||||
` Total Size: ${formatBytes(overallAverage.totalBytes)}`
|
` 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");
|
console.log("\n");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user