testing improvements
This commit is contained in:
2
.gitignore
vendored
2
.gitignore
vendored
@@ -24,6 +24,8 @@ tasks
|
|||||||
# Temp
|
# Temp
|
||||||
gitignore
|
gitignore
|
||||||
#*_migration_source
|
#*_migration_source
|
||||||
|
perf-results-*.json
|
||||||
|
|
||||||
|
|
||||||
# System Files
|
# System Files
|
||||||
.DS_Store
|
.DS_Store
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -64,7 +64,13 @@ const TEST_PAGES: PageTestConfig[] = [
|
|||||||
{ name: "Home", path: "/" },
|
{ name: "Home", path: "/" },
|
||||||
{ name: "About", path: "/about" },
|
{ name: "About", path: "/about" },
|
||||||
{ name: "Blog Index", path: "/blog" },
|
{ name: "Blog Index", path: "/blog" },
|
||||||
{ name: "Blog Post", path: "/blog/A_Journey_in_Self_Hosting" },
|
{ name: "Blog Post (basic)", path: "/blog/I_made_a_macOS_app_in_a_day" },
|
||||||
|
{
|
||||||
|
name: "Blog Post with multiple images",
|
||||||
|
path: "/blog/Shapes_With_Abigail!"
|
||||||
|
},
|
||||||
|
{ 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: "Resume", path: "/resume" },
|
||||||
{ name: "Contact", path: "/contact" }
|
{ name: "Contact", path: "/contact" }
|
||||||
];
|
];
|
||||||
@@ -77,11 +83,102 @@ if (process.env.TEST_BLOG_POST) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function setupPerformanceObservers(page: Page) {
|
||||||
|
await page.addInitScript(() => {
|
||||||
|
(window as any).__perfMetrics = {
|
||||||
|
lcp: 0,
|
||||||
|
cls: 0,
|
||||||
|
fid: 0,
|
||||||
|
largestContentfulPaint: 0,
|
||||||
|
cumulativeLayoutShift: 0,
|
||||||
|
firstInputDelay: 0,
|
||||||
|
layoutShifts: [] as number[],
|
||||||
|
longTasks: [] as number[]
|
||||||
|
};
|
||||||
|
|
||||||
|
// Observe LCP
|
||||||
|
if ("PerformanceObserver" in window) {
|
||||||
|
try {
|
||||||
|
const lcpObserver = new PerformanceObserver((entryList) => {
|
||||||
|
const entries = entryList.getEntries();
|
||||||
|
const lastEntry = entries[entries.length - 1] as any;
|
||||||
|
(window as any).__perfMetrics.lcp =
|
||||||
|
lastEntry.renderTime || lastEntry.loadTime;
|
||||||
|
(window as any).__perfMetrics.largestContentfulPaint =
|
||||||
|
lastEntry.renderTime || lastEntry.loadTime;
|
||||||
|
});
|
||||||
|
lcpObserver.observe({
|
||||||
|
type: "largest-contentful-paint",
|
||||||
|
buffered: true
|
||||||
|
});
|
||||||
|
} catch (e) {
|
||||||
|
// LCP not supported
|
||||||
|
}
|
||||||
|
|
||||||
|
// Observe CLS
|
||||||
|
try {
|
||||||
|
const clsObserver = new PerformanceObserver((entryList) => {
|
||||||
|
for (const entry of entryList.getEntries()) {
|
||||||
|
const layoutShift = entry as any;
|
||||||
|
if (!layoutShift.hadRecentInput) {
|
||||||
|
(window as any).__perfMetrics.cls += layoutShift.value;
|
||||||
|
(window as any).__perfMetrics.cumulativeLayoutShift +=
|
||||||
|
layoutShift.value;
|
||||||
|
(window as any).__perfMetrics.layoutShifts.push(
|
||||||
|
layoutShift.value
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
clsObserver.observe({ type: "layout-shift", buffered: true });
|
||||||
|
} catch (e) {
|
||||||
|
// CLS not supported
|
||||||
|
}
|
||||||
|
|
||||||
|
// Observe FID (first input)
|
||||||
|
try {
|
||||||
|
const fidObserver = new PerformanceObserver((entryList) => {
|
||||||
|
const firstInput = entryList.getEntries()[0] as any;
|
||||||
|
if (firstInput) {
|
||||||
|
(window as any).__perfMetrics.fid =
|
||||||
|
firstInput.processingStart - firstInput.startTime;
|
||||||
|
(window as any).__perfMetrics.firstInputDelay =
|
||||||
|
firstInput.processingStart - firstInput.startTime;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
fidObserver.observe({ type: "first-input", buffered: true });
|
||||||
|
} catch (e) {
|
||||||
|
// FID not supported
|
||||||
|
}
|
||||||
|
|
||||||
|
// Observe long tasks
|
||||||
|
try {
|
||||||
|
const longTaskObserver = new PerformanceObserver((entryList) => {
|
||||||
|
for (const entry of entryList.getEntries()) {
|
||||||
|
(window as any).__perfMetrics.longTasks.push(entry.duration);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
longTaskObserver.observe({ type: "longtask", buffered: true });
|
||||||
|
} catch (e) {
|
||||||
|
// Long tasks not supported
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
async function collectPerformanceMetrics(
|
async function collectPerformanceMetrics(
|
||||||
page: Page
|
page: Page
|
||||||
): Promise<PerformanceMetrics> {
|
): Promise<PerformanceMetrics> {
|
||||||
// Wait for page to be fully loaded
|
// Wait for page to be loaded
|
||||||
await page.waitForLoadState("networkidle");
|
await page.waitForLoadState("load");
|
||||||
|
|
||||||
|
// Wait a bit longer for LCP to settle (it can change as content loads)
|
||||||
|
await page.waitForTimeout(1000);
|
||||||
|
|
||||||
|
// Additional wait for any remaining network activity
|
||||||
|
await page.waitForLoadState("networkidle", { timeout: 5000 }).catch(() => {
|
||||||
|
// Ignore timeout - networkidle may never happen for some pages
|
||||||
|
});
|
||||||
|
|
||||||
// Collect comprehensive performance metrics
|
// Collect comprehensive performance metrics
|
||||||
const metrics = await page.evaluate(() => {
|
const metrics = await page.evaluate(() => {
|
||||||
@@ -91,28 +188,38 @@ async function collectPerformanceMetrics(
|
|||||||
const paint = performance.getEntriesByType("paint");
|
const paint = performance.getEntriesByType("paint");
|
||||||
const fcp = paint.find((entry) => entry.name === "first-contentful-paint");
|
const fcp = paint.find((entry) => entry.name === "first-contentful-paint");
|
||||||
|
|
||||||
// Get LCP using PerformanceObserver
|
// Get metrics from our observers
|
||||||
let lcp = 0;
|
const observedMetrics = (window as any).__perfMetrics || {
|
||||||
let cls = 0;
|
lcp: 0,
|
||||||
let fid = 0;
|
cls: 0,
|
||||||
|
fid: 0,
|
||||||
|
longTasks: []
|
||||||
|
};
|
||||||
|
|
||||||
// Try to get LCP from existing entries
|
// Fallback to direct API if observers didn't capture anything
|
||||||
const lcpEntries = performance.getEntriesByType(
|
let lcp = observedMetrics.lcp;
|
||||||
"largest-contentful-paint"
|
let cls = observedMetrics.cls;
|
||||||
) as any[];
|
let fid = observedMetrics.fid;
|
||||||
if (lcpEntries.length > 0) {
|
|
||||||
lcp =
|
if (lcp === 0) {
|
||||||
lcpEntries[lcpEntries.length - 1].renderTime ||
|
const lcpEntries = performance.getEntriesByType(
|
||||||
lcpEntries[lcpEntries.length - 1].loadTime;
|
"largest-contentful-paint"
|
||||||
|
) as any[];
|
||||||
|
if (lcpEntries.length > 0) {
|
||||||
|
lcp =
|
||||||
|
lcpEntries[lcpEntries.length - 1].renderTime ||
|
||||||
|
lcpEntries[lcpEntries.length - 1].loadTime;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get layout shift entries
|
if (cls === 0) {
|
||||||
const layoutShiftEntries = performance.getEntriesByType(
|
const layoutShiftEntries = performance.getEntriesByType(
|
||||||
"layout-shift"
|
"layout-shift"
|
||||||
) as any[];
|
) as any[];
|
||||||
cls = layoutShiftEntries
|
cls = layoutShiftEntries
|
||||||
.filter((entry: any) => !entry.hadRecentInput)
|
.filter((entry: any) => !entry.hadRecentInput)
|
||||||
.reduce((sum: number, entry: any) => sum + entry.value, 0);
|
.reduce((sum: number, entry: any) => sum + entry.value, 0);
|
||||||
|
}
|
||||||
|
|
||||||
// Get resource timing
|
// Get resource timing
|
||||||
const resources = performance.getEntriesByType(
|
const resources = performance.getEntriesByType(
|
||||||
@@ -157,23 +264,45 @@ async function collectPerformanceMetrics(
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Get performance measure entries for JS execution
|
// Calculate long task duration
|
||||||
const measures = performance.getEntriesByType("measure");
|
|
||||||
let jsExecutionTime = 0;
|
|
||||||
let taskDuration = 0;
|
let taskDuration = 0;
|
||||||
|
if (observedMetrics.longTasks && observedMetrics.longTasks.length > 0) {
|
||||||
|
taskDuration = observedMetrics.longTasks.reduce(
|
||||||
|
(sum: number, duration: number) => sum + duration,
|
||||||
|
0
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get more granular performance entries
|
||||||
|
let jsExecutionTime = 0;
|
||||||
let layoutDuration = 0;
|
let layoutDuration = 0;
|
||||||
let paintDuration = 0;
|
let paintDuration = 0;
|
||||||
|
|
||||||
|
const measures = performance.getEntriesByType("measure");
|
||||||
measures.forEach((entry) => {
|
measures.forEach((entry) => {
|
||||||
if (entry.name.includes("script") || entry.name.includes("js")) {
|
if (entry.name.includes("script") || entry.name.includes("js")) {
|
||||||
jsExecutionTime += entry.duration;
|
jsExecutionTime += entry.duration;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Try to get long task entries
|
// Check for script evaluation entries
|
||||||
const longTasks = performance.getEntriesByType("longtask") as any[];
|
const entries = performance.getEntries();
|
||||||
longTasks.forEach((task: any) => {
|
entries.forEach((entry: any) => {
|
||||||
taskDuration += task.duration;
|
if (entry.entryType === "measure") {
|
||||||
|
if (
|
||||||
|
entry.name.toLowerCase().includes("script") ||
|
||||||
|
entry.name.toLowerCase().includes("js")
|
||||||
|
) {
|
||||||
|
jsExecutionTime += entry.duration;
|
||||||
|
} else if (entry.name.toLowerCase().includes("layout")) {
|
||||||
|
layoutDuration += entry.duration;
|
||||||
|
} else if (
|
||||||
|
entry.name.toLowerCase().includes("paint") ||
|
||||||
|
entry.name.toLowerCase().includes("render")
|
||||||
|
) {
|
||||||
|
paintDuration += entry.duration;
|
||||||
|
}
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@@ -219,7 +348,8 @@ async function testPagePerformance(
|
|||||||
for (let i = 0; i < WARMUP_RUNS; i++) {
|
for (let i = 0; i < WARMUP_RUNS; i++) {
|
||||||
const context = await browser.newContext();
|
const context = await browser.newContext();
|
||||||
const page = await context.newPage();
|
const page = await context.newPage();
|
||||||
await page.goto(url, { waitUntil: "networkidle" });
|
await setupPerformanceObservers(page);
|
||||||
|
await page.goto(url, { waitUntil: "load" });
|
||||||
await page.close();
|
await page.close();
|
||||||
await context.close();
|
await context.close();
|
||||||
console.log(` ✓ Warmup run ${i + 1}/${WARMUP_RUNS}`);
|
console.log(` ✓ Warmup run ${i + 1}/${WARMUP_RUNS}`);
|
||||||
@@ -235,8 +365,11 @@ async function testPagePerformance(
|
|||||||
});
|
});
|
||||||
const page = await context.newPage();
|
const page = await context.newPage();
|
||||||
|
|
||||||
|
// Setup performance observers before navigation
|
||||||
|
await setupPerformanceObservers(page);
|
||||||
|
|
||||||
// Navigate and collect metrics
|
// Navigate and collect metrics
|
||||||
await page.goto(url, { waitUntil: "networkidle" });
|
await page.goto(url, { waitUntil: "load" });
|
||||||
const metrics = await collectPerformanceMetrics(page);
|
const metrics = await collectPerformanceMetrics(page);
|
||||||
|
|
||||||
await page.close();
|
await page.close();
|
||||||
|
|||||||
Reference in New Issue
Block a user