e2e/perf-instrumentation.spec.ts
Linked repository file from this build.
import { expect, test } from "@playwright/test";
import { meter, profiles } from "./perf/measure";
test("the probe waits for data rendering, counts UTF-8 bytes and catches duplicate GETs", async ({
page,
}, info) => {
let release!: () => void;
const gate = new Promise<void>((resolve) => {
release = resolve;
});
const payload = JSON.stringify({ label: "José — 測試", values: Array(100).fill("payload") });
await page.route("**/api/perf-fixture", async (route) => {
await gate;
await route.fulfill({ contentType: "application/json", body: payload });
});
await page.route("**/__perf/target", (route) =>
route.fulfill({
contentType: "text/html",
body: `<h1>Heading already visible</h1><script>
window.payloadReady = false;
fetch('/api/perf-fixture').then(r => r.json()).then(data => { window.payloadReady = true; window.data = data; });
window.renderData = () => { const p = document.createElement('p'); p.id = 'data-ready'; p.textContent = window.data.label; document.body.append(p); };
</script>`,
}),
);
const measure = await meter(page, profiles[0]);
await measure.begin("#data-ready", true, "/__perf/target");
await page.goto("/__perf/target", { waitUntil: "commit" });
await expect(page.getByRole("heading")).toBeVisible();
expect(await page.evaluate(() => window.__qcPerf?.ready)).toBeNull();
release();
await page.waitForFunction(() => (window as unknown as { payloadReady: boolean }).payloadReady);
expect(
await page.evaluate(() => window.__qcPerf?.ready),
"received JSON is not rendered content",
).toBeNull();
const renderedAt = await page.evaluate(() => {
(window as unknown as { renderData(): void }).renderData();
return performance.now();
});
await page.evaluate(() => fetch("/api/perf-fixture").then((r) => r.json()));
const result = await measure.finish(info, "instrumentation", "cold");
expect(result.componentMs).toBeGreaterThanOrEqual(renderedAt);
expect(result.apis).toHaveLength(2);
expect(result.apiJsonBytes).toBe(Buffer.byteLength(payload) * 2);
expect(result.requests.filter((r) => r.path === "/api/perf-fixture")).toHaveLength(2);
expect(result.wireBytes).toBeGreaterThan(result.apiJsonBytes);
});
test("a matching source-page element cannot satisfy destination readiness", async ({
page,
}, info) => {
await page.route("**/__perf/*", (route) =>
route.fulfill({
contentType: "text/html",
body: '<p id="data-ready">Loaded</p>',
}),
);
const measure = await meter(page, profiles[0]);
await page.goto("/__perf/source");
await measure.begin("#data-ready", false, "/__perf/target");
await page.evaluate(
() =>
new Promise<void>((resolve) =>
requestAnimationFrame(() => requestAnimationFrame(() => resolve())),
),
);
expect(await page.evaluate(() => window.__qcPerf?.ready)).toBeNull();
await page.evaluate(() => history.pushState({}, "", "/__perf/target"));
const result = await measure.finish(info, "instrumentation", "navigation");
expect(result.readinessError).toBe("");
expect(result.componentMs).toBeGreaterThan(0);
});
test("the probe records nested fades, content regressions, remounts and main-thread stalls", async ({
page,
}, info) => {
await page.route("**/__perf/smoothness", (route) =>
route.fulfill({
contentType: "text/html",
body: `<style>@keyframes fixture-fade { from { opacity: 0 } to { opacity: 1 } }
.fade { animation: fixture-fade 400ms linear; }</style>
<main class="app-shell"><div class="app-page-content"><div class="fade"><p id="data-ready">Data</p></div></div></main>`,
}),
);
const measure = await meter(page, profiles[0]);
await measure.begin("#data-ready", true, "/__perf/smoothness");
await page.goto("/__perf/smoothness");
await page.waitForFunction(() => window.__qcPerf?.summary().visualReadyMs != null);
await page.evaluate(() => {
document.querySelector("#data-ready")!.setAttribute("hidden", "");
});
await page.waitForFunction(() => window.__qcPerf!.summary().readinessRegressions === 1);
await page.evaluate(() => {
const content = document.querySelector(".app-page-content")!;
const replacement = content.cloneNode(true) as Element;
replacement.querySelector("#data-ready")!.removeAttribute("hidden");
content.replaceWith(replacement);
});
await page.waitForFunction(() => window.__qcPerf?.summary().settledMs != null);
await page.evaluate(() => {
setTimeout(() => {
const end = performance.now() + 120;
while (performance.now() < end) {
/* deliberate browser task, outside CDP evaluation */
}
}, 0);
});
const result = await measure.finish(info, "instrumentation", "cold");
expect(result.smoothness!.visualReadyMs!).toBeGreaterThan(result.componentMs! + 100);
expect(result.smoothness!.readinessRegressions).toBe(1);
expect(result.smoothness!.settledMs!).toBeGreaterThan(result.smoothness!.visualReadyMs!);
expect(result.smoothness!.framesOver50Ms).toBeGreaterThan(0);
expect(result.smoothness!.longTaskBlockingMs).toBeGreaterThan(40);
expect(result.smoothness!.animations.filter((a) => a.name === "fixture-fade")).toHaveLength(2);
expect(new Set(result.smoothness!.timeline.map((t) => t.content).filter(Boolean)).size).toBe(2);
});