perf: subset the icon font and drop the unused second family (#513)

Supersedes #239 by @mml-studio, ported onto the current component layout with fixes from review.

Verified before merge: format, package boundaries, unit suite, build, and the browser tracking suite; a browser harness for the change where one applies; adversarial review applied.

Co-authored-by: Memel <[email protected]>
This commit is contained in:
Bilawal SidhuandMemel authored and GitHub committed 2026-09-14 19:21:38 -05:00
1 parent da700be77f
commit 8702458cf6
4 files changed
+127 -3

No files matched your search

+9
View File
@@ -292,6 +292,15 @@ of current runtime behavior, see [`docs/CURRENT-STATE.md`](docs/CURRENT-STATE.md
### Changed
- The interface asks Google Fonts for only the icon glyphs it draws, instead of
the whole variable icon font, and no longer requests a second icon family that
nothing renders. A check fails when a source names a glyph the request is
missing, because an absent glyph does not draw a placeholder — the element
renders the glyph's name as text. The check reads the panel templates as well
as the scripts, and reads glyph names written as literals, so a glyph chosen
through a variable has to be added to the request by hand. Contributed by
mml-studio (#239).
- Separate explicit browser build settings from standalone environment loading
and local provider middleware. Preserve provider behavior and root named exports.
- Rename standalone browser startup to `src/standalone/` and add a Node-only
+5 -2
View File
@@ -9,8 +9,11 @@
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@300;400;500;600;700&family=Inter:wght@300;400;500;600&display=swap" rel="stylesheet" />
<link href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:opsz,wght,FILL,GRAD@20,400,0,0" rel="stylesheet" />
<link href="https://fonts.googleapis.com/icon?family=Material+Icons+Round" rel="stylesheet" />
<!-- Icon font subset to the glyphs this app renders. Keep `icon_names` in sync
with the sources: src/materialSymbolsSubset.test.mjs fails when a source
names a glyph this list is missing, because a missing glyph does not draw
a box — the ligature never forms and the element renders the literal word. -->
<link href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:opsz,wght,FILL,GRAD@20,400,0,0&icon_names=adjust,arrow_drop_down,arrow_forward,arrow_left,arrow_right,bolt,chevron_left,chevron_right,close,close_fullscreen,dark_mode,draw,east,flare,flight,layers_clear,light_mode,local_fire_department,my_location,navigation,normal,on,open_in_full,public,radar,radio,right_panel_close,right_panel_open,rocket_launch,skip_next,skip_previous,view_in_ar" rel="stylesheet" />
</head>
<body>
<!-- gev:template scene-chrome -->
+2 -1
View File
@@ -614,5 +614,6 @@
"src/layers/vessels/ingestion.test.mjs",
"src/cityPresets.test.mjs",
"server/providers/cctv/range.js",
"src/data/cctvMediaRange.test.mjs"
"src/data/cctvMediaRange.test.mjs",
"src/materialSymbolsSubset.test.mjs"
]
+111
View File
@@ -0,0 +1,111 @@
import { test } from 'node:test';
import assert from 'node:assert/strict';
import { readFileSync, readdirSync } from 'node:fs';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
const SRC_ROOT = fileURLToPath(new URL('.', import.meta.url));
const REPO_ROOT = path.resolve(SRC_ROOT, '..');
const INDEX_HTML = path.join(REPO_ROOT, 'index.html');
/** The glyph written as element text: `<span class="material-symbols-outlined">radar</span>`. */
const SPAN_TEXT =
/class="[^"]*material-symbols-outlined[^"]*"[^>]*>\s*([a-z0-9_]+)\s*</g;
/** A whole `textContent =` statement, across lines, so a multi-line ternary is read once. */
const TEXT_ASSIGNMENT = /(?:textContent|innerText)\s*=\s*([^;]{0,400})/gs;
const STRING_LITERAL = /['"`]([a-z0-9_]{2,})['"`]/g;
/**
* Files that SHIP markup. Tests assert on markup, they do not render it.
*
* The panel markup lives in `src/ui/templates/*.html`, so HTML under `src` is
* read as well: a glyph named only in a template is still a glyph the font has
* to carry.
*/
function sourceFiles(directory = SRC_ROOT) {
const files = [];
for (const entry of readdirSync(directory, { withFileTypes: true })) {
const absolute = path.join(directory, entry.name);
if (entry.isDirectory()) files.push(...sourceFiles(absolute));
else if (
entry.isFile() &&
/\.(js|mjs|html)$/.test(entry.name) &&
!entry.name.endsWith('.test.mjs')
) {
files.push(absolute);
}
}
return [...files.sort(), INDEX_HTML];
}
/**
* Glyph names the sources ask the icon font for.
*
* Errs WIDE on purpose. A name the font does not carry costs nothing — Google
* ignores an unknown `icon_names` entry and still returns 200 — while a glyph
* the subset is missing breaks the interface SILENTLY: the ligature never
* forms, so the element renders the literal word `right_panel_open` instead of
* falling back to a visible box.
*
* A literal to the LEFT of a `?` is the condition being tested, not the text
* being shown, so it is dropped: `status === 'loading' ? …` must not enrol
* `loading`. Optional chaining is neutralised first — `payload?.newsStatus`
* is not a ternary, and splitting on its `?` would keep the condition.
* @returns {Map<string, string>} glyph -> the first file that names it.
*/
function referencedGlyphs() {
const found = new Map();
for (const file of sourceFiles()) {
const source = readFileSync(file, 'utf8');
const relative = path.relative(REPO_ROOT, file).split(path.sep).join('/');
const add = (glyph) => {
if (!found.has(glyph)) found.set(glyph, relative);
};
for (const match of source.matchAll(SPAN_TEXT)) add(match[1]);
for (const match of source.matchAll(TEXT_ASSIGNMENT)) {
const statement = match[1].replaceAll('?.', '.');
const assigned = statement.includes('?')
? statement.slice(statement.indexOf('?') + 1)
: statement;
for (const literal of assigned.matchAll(STRING_LITERAL)) add(literal[1]);
}
}
return found;
}
/** The `icon_names` list index.html asks Google for. */
function subsettedGlyphs(html = readFileSync(INDEX_HTML, 'utf8')) {
const match =
/Material\+Symbols\+Outlined[^"]*[?&]icon_names=([a-z0-9_,]+)/.exec(html);
assert.ok(
match,
'index.html must request Material Symbols with an icon_names subset',
);
return new Set(match[1].split(','));
}
test('every glyph the sources render is in the icon_names subset', () => {
const subset = subsettedGlyphs();
const missing = [...referencedGlyphs()]
.filter(([glyph]) => !subset.has(glyph))
.map(([glyph, file]) => `${glyph} (${file})`);
assert.deepEqual(
missing,
[],
'Glyphs named by the sources but absent from the index.html icon_names list. ' +
'Add them there — an unlisted glyph renders as its own name on screen: ' +
missing.join(', '),
);
});
test('the unused Material Icons Round family is not loaded', () => {
// A second icon font, 173 kB, for a family no source ever uses — and
// src/cockpitMarkup.test.mjs already asserts the markup must not use it.
const html = readFileSync(INDEX_HTML, 'utf8');
assert.doesNotMatch(
html,
/Material\+Icons\+Round/,
'index.html loads an icon font nothing renders',
);
});