mirror of
https://github.com/4gray/iptvnator.git
synced 2026-10-08 17:06:15 -08:00
fix(performance): count each initial request URL and soften the CI wording
Deduplicate initial resources by request URL rather than by file, so a file referenced twice with different query strings is counted twice, as the browser fetches it; fragments are still ignored. Each resource now carries both its file path and its request URL. Describe the CI ratchet as the goal of the loop rather than as existing enforcement until the baseline and job PRs merge. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
1 parent
3cd032510e
commit
5cb69b1ec1
4 files changed
+50
-26
No files matched your search
@@ -377,7 +377,7 @@ $ pnpm run serve:frontend
|
||||
```
|
||||
|
||||
To see how many bytes the built web app puts on the initial load path (the
|
||||
number CI ratchets), build it and run the measurement:
|
||||
number the performance ratchet guards), build it and run the measurement:
|
||||
|
||||
```
|
||||
$ pnpm nx build web
|
||||
|
||||
@@ -2,10 +2,12 @@
|
||||
|
||||
IPTVnator measures performance through a small set of everyday user journeys.
|
||||
Each journey has deterministic counters that are asserted exactly, and
|
||||
wall-clock timings that are recorded as evidence. Counters are ratcheted in CI:
|
||||
a committed baseline may only be lowered, and only with the measured output as
|
||||
evidence. This document is the contract for that loop; `tools/performance/`
|
||||
holds the scripts.
|
||||
wall-clock timings that are recorded as evidence. Counters are meant to be
|
||||
ratcheted in CI: a committed baseline that may only be lowered, and only with
|
||||
the measured output as evidence. This document is the contract for that loop;
|
||||
`tools/performance/` holds the scripts. The measurement script lands first;
|
||||
the baseline file and the CI job follow in their own PRs (#1693, #1694), so
|
||||
until they merge the reported number is informational, not enforced.
|
||||
|
||||
## Journeys
|
||||
|
||||
|
||||
@@ -58,8 +58,10 @@ function classify(tag, attributes) {
|
||||
|
||||
/**
|
||||
* Lists the same-origin resources `index.html` puts on the initial path, in
|
||||
* document order and without duplicates. Query strings and fragments are
|
||||
* stripped so the URL maps to a file on disk.
|
||||
* document order. Duplicates are collapsed by request URL, not by file: the
|
||||
* browser fetches `chunk.js?v=1` and `chunk.js?v=2` separately, so both count,
|
||||
* while a fragment never reaches the server and is ignored. `path` is the
|
||||
* file on disk the URL maps to.
|
||||
*/
|
||||
export function extractInitialResources(html) {
|
||||
const seen = new Set();
|
||||
@@ -70,10 +72,11 @@ export function extractInitialResources(html) {
|
||||
parseAttributes(rawAttributes)
|
||||
);
|
||||
if (!resource || EXTERNAL_URL.test(resource.url)) continue;
|
||||
const file = resource.url.replace(/[?#].*$/, '').replace(/^\.?\//, '');
|
||||
if (!file || seen.has(file)) continue;
|
||||
seen.add(file);
|
||||
resources.push({ path: file, kind: resource.kind });
|
||||
const url = resource.url.replace(/#.*$/, '').replace(/^\.?\//, '');
|
||||
const file = url.replace(/\?.*$/, '');
|
||||
if (!file || seen.has(url)) continue;
|
||||
seen.add(url);
|
||||
resources.push({ path: file, url, kind: resource.kind });
|
||||
}
|
||||
return resources;
|
||||
}
|
||||
@@ -160,7 +163,7 @@ function formatBytes(bytes) {
|
||||
export function formatReport(measurement) {
|
||||
const rows = [...measurement.resources].sort((a, b) => b.bytes - a.bytes);
|
||||
const width = Math.max(
|
||||
...rows.map((row) => row.path.length),
|
||||
...rows.map((row) => row.url.length),
|
||||
'index.html'.length
|
||||
);
|
||||
const lines = [
|
||||
@@ -169,7 +172,7 @@ export function formatReport(measurement) {
|
||||
`${'index.html'.padEnd(width)} html ${formatBytes(measurement.indexHtml.bytes).padStart(11)}`,
|
||||
...rows.map(
|
||||
(row) =>
|
||||
`${row.path.padEnd(width)} ${row.kind.padEnd(13)} ${formatBytes(row.bytes).padStart(11)}`
|
||||
`${row.url.padEnd(width)} ${row.kind.padEnd(13)} ${formatBytes(row.bytes).padStart(11)}`
|
||||
),
|
||||
'',
|
||||
`scripts ${formatBytes(measurement.totals.script).padStart(11)}`,
|
||||
|
||||
@@ -69,14 +69,20 @@ after(async () => {
|
||||
});
|
||||
|
||||
test('extracts scripts, stylesheets and modulepreload chunks in document order', () => {
|
||||
assert.deepEqual(extractInitialResources(BUILT_INDEX_HTML), [
|
||||
{ path: 'assets/app-config.js', kind: 'script' },
|
||||
{ path: 'styles-VDU4SQ5F.css', kind: 'stylesheet' },
|
||||
{ path: 'chunk-B6uziQ1i.js', kind: 'modulepreload' },
|
||||
{ path: 'chunk-Cn2Agfvf.js', kind: 'modulepreload' },
|
||||
{ path: 'polyfills-EBB6HFCX.js', kind: 'script' },
|
||||
{ path: 'main-EI6PCDGR.js', kind: 'script' },
|
||||
]);
|
||||
assert.deepEqual(
|
||||
extractInitialResources(BUILT_INDEX_HTML).map(({ path, kind }) => ({
|
||||
path,
|
||||
kind,
|
||||
})),
|
||||
[
|
||||
{ path: 'assets/app-config.js', kind: 'script' },
|
||||
{ path: 'styles-VDU4SQ5F.css', kind: 'stylesheet' },
|
||||
{ path: 'chunk-B6uziQ1i.js', kind: 'modulepreload' },
|
||||
{ path: 'chunk-Cn2Agfvf.js', kind: 'modulepreload' },
|
||||
{ path: 'polyfills-EBB6HFCX.js', kind: 'script' },
|
||||
{ path: 'main-EI6PCDGR.js', kind: 'script' },
|
||||
]
|
||||
);
|
||||
});
|
||||
|
||||
test('ignores manifest, icon and external references', () => {
|
||||
@@ -90,24 +96,37 @@ test('ignores manifest, icon and external references', () => {
|
||||
<script>inline()</script>
|
||||
<script src="main.js" type="module"></script>`;
|
||||
assert.deepEqual(extractInitialResources(html), [
|
||||
{ path: 'main.js', kind: 'script' },
|
||||
{ path: 'main.js', url: 'main.js', kind: 'script' },
|
||||
]);
|
||||
});
|
||||
|
||||
test('deduplicates references and normalizes relative URLs', () => {
|
||||
test('deduplicates by request URL, ignores fragments and normalizes relative URLs', () => {
|
||||
const html = `
|
||||
<LINK REL="modulepreload" HREF='./chunk-a.js'>
|
||||
<link rel="modulepreload" href="chunk-a.js">
|
||||
<link rel="modulepreload" href="chunk-a.js?v=2">
|
||||
<link rel="modulepreload" href="/chunk-b.js#hash">
|
||||
<link rel="modulepreload" href="chunk-b.js#other">
|
||||
<script src=main.js></script>
|
||||
<script src="main.js"></script>`;
|
||||
assert.deepEqual(extractInitialResources(html), [
|
||||
{ path: 'chunk-a.js', kind: 'modulepreload' },
|
||||
{ path: 'chunk-b.js', kind: 'modulepreload' },
|
||||
{ path: 'main.js', kind: 'script' },
|
||||
{ path: 'chunk-a.js', url: 'chunk-a.js', kind: 'modulepreload' },
|
||||
{ path: 'chunk-a.js', url: 'chunk-a.js?v=2', kind: 'modulepreload' },
|
||||
{ path: 'chunk-b.js', url: 'chunk-b.js', kind: 'modulepreload' },
|
||||
{ path: 'main.js', url: 'main.js', kind: 'script' },
|
||||
]);
|
||||
});
|
||||
|
||||
test('counts a file once per distinct request URL', async () => {
|
||||
const distDir = await writeDist('cache-busted', {
|
||||
indexHtml: `<link rel="modulepreload" href="chunk-a.js"><link rel="modulepreload" href="chunk-a.js?v=2">`,
|
||||
files: { 'chunk-a.js': 100 },
|
||||
});
|
||||
const measurement = await measureInitialBytes({ distDir });
|
||||
assert.equal(measurement.resources.length, 2);
|
||||
assert.equal(measurement.totals.modulepreload, 200);
|
||||
});
|
||||
|
||||
test('sums index.html and every referenced file into the counter', async () => {
|
||||
const distDir = await writeDist('built');
|
||||
const measurement = await measureInitialBytes({ distDir });
|
||||
|
||||
Reference in new issue
Block a user