fix(performance): ignore inert HTML when listing initial resources

Strip HTML comments and the bodies of inline script/style elements before
scanning index.html, so a commented-out tag or a tag-like string literal
is neither counted nor reported as a missing file. Document the silent
pnpm invocation for the JSON output and state that the counter is
Angular's "Initial total" plus index.html and app-config.js rather than
an exact match of that rounded figure.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
4grayandClaude Fable 5.1 committed 2026-09-26 10:48:50 +02:00
1 parent 5cb69b1ec1
commit be58eacb20
3 files changed
+45 -9

No files matched your search

+11 -8
View File
@@ -32,16 +32,19 @@ The bytes a browser fetches before Angular can bootstrap, read from the built
- every `<link rel="stylesheet">`,
- every `<link rel="modulepreload">` chunk.
Manifest, icons, external URLs and lazy chunks are not counted. A file that
`index.html` references but the build did not emit is an error, never zero
bytes. The value is raw (uncompressed) size, which is what the renderer parses;
it matches the "Initial total" line of the Angular build output.
Manifest, icons, external URLs, commented-out tags and lazy chunks are not
counted. A file that `index.html` references but the build did not emit is an
error, never zero bytes. The value is raw (uncompressed) size, which is what the
renderer parses. It is Angular's "Initial total" plus `index.html` and
`assets/app-config.js` (about 4 KB together), so it sits slightly above the
rounded figure the build prints; never copy that figure into a baseline, use
the script's output.
```bash
pnpm nx build web # production configuration
pnpm run perf:initial-bytes # human-readable breakdown
pnpm run perf:initial-bytes -- --json # machine-readable breakdown
pnpm run perf:initial-bytes -- --summary dist/performance/journey-summary.json
pnpm nx build web # production configuration
pnpm run perf:initial-bytes # human-readable breakdown
pnpm --silent run perf:initial-bytes -- --json # machine-readable; --silent keeps pnpm's headers out of stdout
node tools/performance/measure-initial-bytes.mjs --summary dist/performance/journey-summary.json
```
`--summary` writes the journey summary shape (`journeys.<journey>.counters`)
+16 -1
View File
@@ -21,6 +21,8 @@ export const INITIAL_BYTES_COUNTER = 'renderer.initialBytes';
export const LAUNCH_JOURNEY = 'launch';
const TAG_PATTERN = /<(script|link)\b([^>]*)>/gi;
const HTML_COMMENT = /<!--[\s\S]*?-->/g;
const INLINE_ELEMENT_BODY = /(<(script|style)\b[^>]*>)[\s\S]*?(<\/\2\s*>)/gi;
const ATTRIBUTE_PATTERN =
/([a-zA-Z_:][-a-zA-Z0-9_:.]*)(?:\s*=\s*(?:"([^"]*)"|'([^']*)'|([^\s"'=<>`]+)))?/g;
const EXTERNAL_URL = /^(?:[a-z][a-z0-9+.-]*:|\/\/)/i;
@@ -56,6 +58,17 @@ function classify(tag, attributes) {
return null;
}
/**
* Removes the parts of the document the browser never treats as markup: HTML
* comments (a commented-out `<script>` is not a request) and the bodies of
* inline `<script>`/`<style>` elements (a string literal that looks like a tag
* is not one either). The opening tags themselves are kept so a `src` on an
* inline script element is still seen.
*/
export function stripInertHtml(html) {
return html.replace(HTML_COMMENT, '').replace(INLINE_ELEMENT_BODY, '$1$3');
}
/**
* Lists the same-origin resources `index.html` puts on the initial path, in
* document order. Duplicates are collapsed by request URL, not by file: the
@@ -66,7 +79,9 @@ function classify(tag, attributes) {
export function extractInitialResources(html) {
const seen = new Set();
const resources = [];
for (const [, tag, rawAttributes] of html.matchAll(TAG_PATTERN)) {
for (const [, tag, rawAttributes] of stripInertHtml(html).matchAll(
TAG_PATTERN
)) {
const resource = classify(
tag.toLowerCase(),
parseAttributes(rawAttributes)
@@ -13,6 +13,7 @@ import {
formatReport,
measureInitialBytes,
parseArgs,
stripInertHtml,
toJourneySummary,
} from './measure-initial-bytes.mjs';
@@ -117,6 +118,23 @@ test('deduplicates by request URL, ignores fragments and normalizes relative URL
]);
});
test('ignores commented-out tags and tag-like text inside inline scripts and styles', () => {
const html = `
<!-- <script src="old.js"></script> -->
<!--
<link rel="stylesheet" href="legacy.css">
-->
<script>const markup = '<script src="fake.js"><\\/script><link rel="modulepreload" href="fake-chunk.js">';</script>
<style>/* <link rel="stylesheet" href="fake.css"> */ body { color: red; }</style>
<script src="assets/app-config.js" defer></script>
<script src="main.js" type="module"></script>`;
assert.deepEqual(
extractInitialResources(html).map((resource) => resource.url),
['assets/app-config.js', 'main.js']
);
assert.equal(stripInertHtml('<script>1 < 2</script>'), '<script></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">`,