Repository Public
1import { expect, test } from "@playwright/test";23/** Basic performance budget for the first viewport. Deliberately loose: it catches regressions, not milliseconds. */4test("home: small document, fast first paint, lean JavaScript", async ({ page }) => {5 const sizes: { url: string; type: string; bytes: number }[] = [];6 page.on("response", async (r) => {7 const type = r.request().resourceType();8 if (type === "document" || type === "script" || type === "stylesheet" || type === "font") {9 const len = (await r.body().catch(() => Buffer.alloc(0))).length; // decoded size, not transfer size10 sizes.push({ url: r.url(), type, bytes: len });11 }12 });13 await page.goto("/", { waitUntil: "load" });14 const nav = await page.evaluate(() => {15 const n = performance.getEntriesByType("navigation")[0] as PerformanceNavigationTiming;16 const fcp = performance.getEntriesByName("first-contentful-paint")[0]?.startTime ?? null;17 return { ttfb: n.responseStart - n.requestStart, dcl: n.domContentLoadedEventEnd, fcp };18 });19 const total = (type: string) => sizes.filter((s) => s.type === type).reduce((n, s) => n + s.bytes, 0);20 console.log(`ttfb ${Math.round(nav.ttfb)} ms · DCL ${Math.round(nav.dcl)} ms · FCP ${nav.fcp === null ? "n/a" : Math.round(nav.fcp) + " ms"} · js ${Math.round(total("script") / 1024)} kB · css ${Math.round(total("stylesheet") / 1024)} kB (decoded)`);21 expect(nav.ttfb).toBeLessThan(1500);22 expect(nav.dcl).toBeLessThan(4000);23 if (nav.fcp !== null) expect(nav.fcp).toBeLessThan(3000);24 expect(total("script")).toBeLessThan(650 * 1024); // framework runtime + player, uncompressed25 expect(total("stylesheet")).toBeLessThan(120 * 1024);26});2728test("the hero video is not downloaded in full before it plays", async ({ page, request }) => {29 const films = (await (await request.get("/api/films?limit=1")).json()).films;30 test.skip(!films.length, "no published film");31 let videoBytes = 0;32 page.on("response", (r) => { if (r.url().includes("/media/") && r.url().endsWith(".mp4")) videoBytes += Number(r.headers()["content-length"] ?? 0); });33 await page.goto("/", { waitUntil: "domcontentloaded" });34 await page.waitForTimeout(1500);35 expect(videoBytes).toBeLessThanOrEqual(films[0].bytes + 1024);36});37