Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions docs/engineering-practices.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
3 changes: 2 additions & 1 deletion ui/scripts/check-bundle-size.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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`);
Expand Down
8 changes: 8 additions & 0 deletions ui/src/stubs/linkify-it.ts
Original file line number Diff line number Diff line change
@@ -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;
}
3 changes: 3 additions & 0 deletions ui/vite.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 } },
Expand Down
Loading