mirror of
https://github.com/jcreek/LivingDexTracker.git
synced 2026-09-16 18:42:03 +00:00
perf: fix slow page loads and gate performance in CI
- Ship one hashed Tailwind stylesheet instead of two (one render-blocking) - Compress responses in-app (brotli/gzip, streaming-safe) and precompress the node build so local and CI measurements match production - Validate the Supabase session once per request - Render the homepage immediately and stream public stats - Stream a Pokedex's entries with the page instead of fetching after hydration, running the rows and count queries in parallel - Shrink the avatar and offline placeholder images, fix layout shift, contrast, link names and missing meta descriptions - Add Lighthouse CI (mobile + desktop) with score and metric budgets, bundle-size budgets in the build tests, and signed-in speed scenarios
This commit is contained in:
@@ -0,0 +1,77 @@
|
||||
import { createBdd } from 'playwright-bdd';
|
||||
import type { Page } from '@playwright/test';
|
||||
import { test, expect } from '../fixtures';
|
||||
|
||||
const { When, Then } = createBdd(test);
|
||||
|
||||
// Per-page scratch values; scenarios run one at a time (workers: 1).
|
||||
const timings = new WeakMap<
|
||||
Page,
|
||||
{ entriesMs?: number; switchMs: number[]; entryRequests: number }
|
||||
>();
|
||||
|
||||
function entriesVisible(page: Page) {
|
||||
return expect(page.getByRole('button', { name: /^View details for / }).first()).toBeVisible({
|
||||
timeout: 30_000
|
||||
});
|
||||
}
|
||||
|
||||
When('I load the Pokédex page directly', async ({ page, state }) => {
|
||||
if (!state.pokedexId) throw new Error('A Pokédex must exist before it can be opened');
|
||||
const record = { switchMs: [], entryRequests: 0 };
|
||||
timings.set(page, record);
|
||||
// Only requests made while the page first loads matter; the page's 60s reconciliation refetch
|
||||
// can't fire within this window.
|
||||
const countEntryRequests = (request: { url(): string }) => {
|
||||
if (/\/api\/pokedexes\/[^/]+\/combined-data/.test(request.url())) record.entryRequests++;
|
||||
};
|
||||
page.on('request', countEntryRequests);
|
||||
|
||||
const started = Date.now();
|
||||
await page.goto(`/pokedex/${state.pokedexId}`);
|
||||
await entriesVisible(page);
|
||||
(record as { entriesMs?: number }).entriesMs = Date.now() - started;
|
||||
page.off('request', countEntryRequests);
|
||||
});
|
||||
|
||||
Then('its entries appear within {int} seconds', async ({ page }, seconds: number) => {
|
||||
const entriesMs = timings.get(page)?.entriesMs;
|
||||
expect(entriesMs, 'entries never became visible').toBeDefined();
|
||||
expect(entriesMs!).toBeLessThan(seconds * 1000);
|
||||
});
|
||||
|
||||
Then('the browser did not request the entries separately', async ({ page }) => {
|
||||
// The server load streams the first page of entries with the HTML, so the page must not make
|
||||
// the old hydrate-then-fetch round trip.
|
||||
expect(timings.get(page)?.entryRequests).toBe(0);
|
||||
});
|
||||
|
||||
When('I switch between my Pokédex list and the Pokédex', async ({ page, state }) => {
|
||||
if (!state.pokedexName) throw new Error('A Pokédex must exist before switching to it');
|
||||
const record = { switchMs: [] as number[], entryRequests: 0 };
|
||||
timings.set(page, record);
|
||||
await page.goto('/my-pokedexes');
|
||||
const card = page.locator('.card').filter({ hasText: state.pokedexName }).first();
|
||||
await expect(card).toBeVisible();
|
||||
|
||||
for (let round = 0; round < 2; round++) {
|
||||
// List -> Pokédex: a client-side navigation through the card's View button.
|
||||
let started = Date.now();
|
||||
await card.getByRole('button', { name: 'View', exact: true }).click();
|
||||
await page.waitForURL('**/pokedex/**');
|
||||
await entriesVisible(page);
|
||||
record.switchMs.push(Date.now() - started);
|
||||
|
||||
// Pokédex -> list: back navigation is also handled by the client router.
|
||||
started = Date.now();
|
||||
await page.goBack();
|
||||
await expect(card).toBeVisible();
|
||||
record.switchMs.push(Date.now() - started);
|
||||
}
|
||||
});
|
||||
|
||||
Then('each switch finishes within {int} seconds', async ({ page }, seconds: number) => {
|
||||
const switchMs = timings.get(page)?.switchMs ?? [];
|
||||
expect(switchMs).toHaveLength(4);
|
||||
for (const ms of switchMs) expect(ms).toBeLessThan(seconds * 1000);
|
||||
});
|
||||
Reference in New Issue
Block a user