mirror of
https://github.com/hanzo-apps/hanzo.ai.git
synced 2026-10-29 07:20:43 +00:00
91 lines
3.9 KiB
TypeScript
91 lines
3.9 KiB
TypeScript
import { expect, test } from '@playwright/test'
|
|
import { readFileSync } from 'node:fs'
|
|
import { join } from 'node:path'
|
|
import { brotliCompressSync } from 'node:zlib'
|
|
import { OUT, pages, serveExport } from './export'
|
|
import { measure, profiles, viewports } from '../../scripts/perf-measure.mjs'
|
|
|
|
/**
|
|
* A PAGE THAT GETS HEAVIER FAILS THE MERGE THAT MADE IT SO.
|
|
*
|
|
* `perf.budget.json` holds the limits: `js` is the first-party JavaScript a page
|
|
* downloads before it is interactive, in KB after brotli; `lcp` is milliseconds
|
|
* to the largest paint on Fast 3G with a 4x slower CPU; `cls` is layout shift.
|
|
* `default` covers every page, `pages` raises one, and `vitals` names the pages
|
|
* driven in a browser.
|
|
*
|
|
* The bytes are read off the export, so every shipped page is held to its limit
|
|
* for the price of reading files. The paint is measured in Chromium over the
|
|
* same export, served with brotli, because a byte count says nothing about
|
|
* whether the headline waits for a face that is queued behind the scripts.
|
|
* Third-party tags load after `load` and are not in the JS figure.
|
|
*/
|
|
type Limit = { js: number; lcp: number; cls: number }
|
|
const budget: { default: Limit; pages?: Record<string, Partial<Limit>>; vitals: string[] } = JSON.parse(
|
|
readFileSync(join(OUT, '..', 'perf.budget.json'), 'utf8'),
|
|
)
|
|
const limit = (route: string): Limit => ({ ...budget.default, ...budget.pages?.[route] })
|
|
|
|
const sizes = new Map<string, number>()
|
|
const brotli = (src: string): number => {
|
|
let size = sizes.get(src)
|
|
if (size === undefined) {
|
|
size = brotliCompressSync(readFileSync(join(OUT, decodeURIComponent(src.split('?')[0])))).length
|
|
sizes.set(src, size)
|
|
}
|
|
return size
|
|
}
|
|
|
|
/** Script URLs a browser fetches to load `html`: the `nomodule` fallback is not one. */
|
|
const scripts = (html: string): string[] => [
|
|
...new Set(
|
|
[...html.matchAll(/<script\b[^>]*\bsrc="(\/[^"]+\.js[^"]*)"[^>]*>/g)]
|
|
.filter((m) => !/\bnoModule\b/i.test(m[0]))
|
|
.map((m) => m[1]),
|
|
),
|
|
]
|
|
|
|
test('no page ships more first-party JavaScript than its budget', () => {
|
|
const over: string[] = []
|
|
for (const { route, file } of pages()) {
|
|
const kb = scripts(readFileSync(file, 'utf8')).reduce((sum, src) => sum + brotli(src), 0) / 1024
|
|
const cap = limit(route).js
|
|
if (kb > cap) over.push(`${route}: ${kb.toFixed(0)} KB > ${cap} KB`)
|
|
}
|
|
expect(over, 'raise perf.budget.json only for a page that earns its weight').toEqual([])
|
|
})
|
|
|
|
test.describe('the first paint', () => {
|
|
test.describe.configure({ mode: 'serial' })
|
|
let base: string
|
|
let stop: () => Promise<void>
|
|
test.beforeAll(async () => {
|
|
const served = await serveExport({ compress: true })
|
|
base = served.url
|
|
stop = served.close
|
|
})
|
|
test.afterAll(async () => stop())
|
|
|
|
for (const route of budget.vitals) {
|
|
test(`${route} paints and holds still within budget on Fast 3G`, async ({ browser }) => {
|
|
test.setTimeout(300_000)
|
|
const cap = limit(route)
|
|
// The best of three per viewport: a shared machine only ever adds time, so
|
|
// the fastest load says what the page costs. -1 is a load that never
|
|
// painted, which is a failure and not a fast page.
|
|
const best = async (viewport: (typeof viewports)[keyof typeof viewports]) => {
|
|
const runs = []
|
|
for (let i = 0; i < 3; i++) runs.push(await measure(browser, base, route, viewport, profiles.fast3g))
|
|
const painted = runs.filter((r) => r.lcp > 0)
|
|
return { lcp: painted.length ? Math.min(...painted.map((r) => r.lcp)) : -1, cls: Math.min(...runs.map((r) => r.cls)) }
|
|
}
|
|
for (const [name, viewport] of Object.entries(viewports)) {
|
|
const got = await best(viewport)
|
|
expect(got.lcp, `${route} ${name} painted`).toBeGreaterThan(0)
|
|
expect(got.lcp, `${route} ${name} LCP ms`).toBeLessThanOrEqual(cap.lcp)
|
|
expect(got.cls, `${route} ${name} CLS`).toBeLessThanOrEqual(cap.cls)
|
|
}
|
|
})
|
|
}
|
|
})
|