From dbdb762412e5bec5925ff9fd89cc637063f6880c Mon Sep 17 00:00:00 2001 From: qte77 <93844790+qte77@users.noreply.github.com> Date: Thu, 29 Oct 2026 15:23:45 +0000 Subject: [PATCH] perf(ui): stub unused linkify-it and lift JS ceiling to 155 kB (#350) markdown-it (via @a2ui/react) builds a LinkifyIt but never calls it with the default preset, so alias it to an empty stub: 149,694 -> 146,898 B gzip. The size gate sums every chunk, react-dom/zod/markdown-it are outside our control, so the JS ceiling moves 150 -> 155 kB with the measured breakdown recorded in docs/engineering-practices.md. Closes #350 Co-Authored-By: Claude --- docs/engineering-practices.md | 9 +++++++++ ui/scripts/check-bundle-size.mjs | 3 ++- ui/src/stubs/linkify-it.ts | 8 ++++++++ ui/vite.config.ts | 3 +++ 4 files changed, 22 insertions(+), 1 deletion(-) create mode 100644 ui/src/stubs/linkify-it.ts diff --git a/docs/engineering-practices.md b/docs/engineering-practices.md index 474a40e..aecf70a 100644 --- a/docs/engineering-practices.md +++ b/docs/engineering-practices.md @@ -31,6 +31,15 @@ Overriding principle: **measure and verify on the live target — not vibes, not transfer win you can't measure. - **Guard the win in CI.** A gzip bundle-size ceiling (`ui/scripts/check-bundle-size.mjs`, node `zlib`, no new dependency) fails the build if JS/CSS regress past a threshold. + It sums **every** emitted chunk, so lazy-loading moves bytes but never creates headroom. +- **JS ceiling 150 → 155 kB gzip (#350, measured 2026-10-29).** At 149,694 B (306 B headroom) the + breakdown was: react-dom (fixed cost) · zod (pinned by `@a2ui/web_core`'s v0_8 schema, so not + ours to drop) · the markdown-it stack that `@a2ui/react`'s Text renders through (≈46 kB gzip + incl. its `entities` decode table) · app code ≈15 kB. The only behaviour-neutral trim was + aliasing `linkify-it` to an empty stub (markdown-it builds it but never calls it with linkify + off): −2.8 kB → 146,891 B. Replacing the markdown engine itself would reclaim ~46 kB but changes + what text renders (and its XSS surface), so it's a deliberate owner call, not a trim — the + ceiling moved instead, leaving ~8 kB of headroom. - **Measure mobile, before/after, on the deploy.** Throttled (Fast-3G + 4× CPU) LCP/FCP/CLS via a real headless browser against the live URL. Report honest deltas — a page already in the "good" band barely moves; the value is fewer bytes + caching. diff --git a/ui/scripts/check-bundle-size.mjs b/ui/scripts/check-bundle-size.mjs index 3938cf4..05116b9 100644 --- a/ui/scripts/check-bundle-size.mjs +++ b/ui/scripts/check-bundle-size.mjs @@ -12,7 +12,8 @@ const ASSETS = fileURLToPath(new URL("../dist/assets/", import.meta.url)); // Gzip byte ceilings for the entry bundles: current size + headroom. Raise // deliberately (in the same PR) when an intentional feature grows the bundle. -const LIMITS = { ".js": 150_000, ".css": 8_000 }; +// JS 150k → 155k (#350): measured breakdown + reason in docs/engineering-practices.md § Performance. +const LIMITS = { ".js": 155_000, ".css": 8_000 }; if (!existsSync(ASSETS)) { console.error(`no build output at ${ASSETS} — run \`npm run build\` first`); diff --git a/ui/src/stubs/linkify-it.ts b/ui/src/stubs/linkify-it.ts new file mode 100644 index 0000000..19d8b38 --- /dev/null +++ b/ui/src/stubs/linkify-it.ts @@ -0,0 +1,8 @@ +// Build-time stand-in for `linkify-it` (aliased in vite.config.ts, #350). markdown-it constructs a +// LinkifyIt in its constructor but only calls it when `linkify: true`; @a2ui/react builds its renderer +// with `new MarkdownIt()` (default preset, linkify off), so the real library is dead weight (~2.8 kB +// gzip). If a future @a2ui/react turns linkify on, markdown-it calls a method this class lacks and the +// Text render throws — loud in the e2e sweep, never a silent mis-render. +export default class LinkifyIt { + readonly stub = true; +} diff --git a/ui/vite.config.ts b/ui/vite.config.ts index 67a5e43..7aef947 100644 --- a/ui/vite.config.ts +++ b/ui/vite.config.ts @@ -9,6 +9,9 @@ export default defineConfig({ // CF Pages serves at the domain root (sortmy.london). Override with VITE_BASE if ever needed. base: nodeEnv.VITE_BASE ?? "/", plugins: [react(), tailwindcss()], + // #350: markdown-it (pulled in by @a2ui/react) imports linkify-it unconditionally, but the default + // preset @a2ui/react uses never calls it — swap in an empty stub (see src/stubs/linkify-it.ts). + resolve: { alias: { "linkify-it": "/src/stubs/linkify-it.ts" } }, server: { // Same-origin dev: the SPA fetches /api/*, Vite proxies it to the local `wrangler dev` worker. proxy: { "/api": { target: "http://localhost:8787", changeOrigin: true } },