managarten/docs/optimizable/bundle-analysis.md
Till JS 3b85d7d3d2 chore(bundle): add bundle-size audit + snapshot inventory
scripts/audit-bundle.mjs reads `.svelte-kit/output/client/_app/immutable`
after a prod build and reports:
  - Total size + category breakdown (entry / nodes / chunks / workers /
    assets).
  - Top N JS files with content heuristics (transformers.js, zxcvbn,
    tiptap, pdf-lib, swissqrbill, rrule, suncalc, Phosphor icon paths,
    Vite __vite__mapDeps metadata, etc).
  - Route mapping for `nodes/*.js` by parsing the server manifest's
    `leaf:` entries, so node 118 is identified as /(app)/invoices/[id].
  - ⚠ flag on chunks/ ≥ 200 KB (shared, potentially eager).

Current snapshot (docs/optimizable/bundle-analysis.md):
  entry   92 KB  |  nodes   2.77 MB  |  chunks   5.59 MB
  workers 22.3 MB (ONNX WASM, lazy)  |  total   31.8 MB

Already healthy:
  - 92 KB entry (no critical-path bloat).
  - 22 MB transformers.js WASM is worker-scoped — only fetched on first
    /llm-test or memoro voice use.
  - zxcvbn (1.25 MB combined dict + keyboard graphs) correctly behind a
    dynamic import in PasswordStrength.svelte.

Follow-up opportunities logged:
  1. /invoices/[id] = 534 KB — split swissqrbill + pdf-lib via dynamic
     import.
  2. @mana/shared-icons = 317 + 149 KB SVG path chunks — migrate to
     tree-shakable per-icon imports or lazy-load.
  3. Root (app) layout = 260 KB — check for module bleed into shared
     shell.

Report-only. Run with `pnpm run audit:bundle`.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-22 17:52:08 +02:00

4.1 KiB

Bundle Analysis

Snapshot 2026-04-22. Run pnpm run audit:bundle after any pnpm --filter @mana/web build for live numbers.

Snapshot

Category Size % Notes
entry 92 KB 0% app.js + start.js — first JS a cold browser loads
nodes 2.77 MB 9% per-route layout/page bundles (230 files)
chunks 5.59 MB 18% shared code-split modules (711 files)
workers 22.31 MB 70% transformers.js ONNX WASM — lazy until LLM/STT opened
assets 1.04 MB 3% CSS, fonts, images
total 31.80 MB 1147 files

Entry is tiny (92 KB). That's healthy. The 22 MB in workers/ is ort-wasm-simd-threaded.asyncify-*.wasm — the ONNX Runtime for transformers.js. It's only fetched when the browser actually instantiates the Web Worker (first use of @mana/local-llm or @mana/local-stt). Most users never hit it.

Biggest shared chunks

Top entries in chunks/ ≥ 200 KB, with what's actually inside:

Size File Content Verdict
797 KB SDMVbHi1 @zxcvbn-ts/language-de German dictionary lazy — dynamic import in PasswordStrength.svelte, only on register / recovery
454 KB bdamX4EN @zxcvbn-ts/language-common keyboard adjacency graphs lazy — same import path
317 KB DtX-t1si @mana/shared-icons (Phosphor SVG paths) partly eager — imported by many routes; see notes
220 KB BbeX9yAb Vite __vite__mapDeps import-graph metadata only, not real code
162 KB Bqmpszdn (unknown) below threshold

Biggest route bundles

Routes loaded per navigation (not eagerly):

Size Route Note
534 KB /(app)/invoices/[id] heaviest route. Likely swissqrbill + pdf-lib. Investigate — could split the QR-bill generator behind await import() so preview/list pages stay small.
380 KB /(app)/broadcasts/[id]/edit Tiptap editor — unavoidable, tiptap is ~250 KB baseline
260 KB node 2 (root (app) layout) All app-wide chrome: shell, stores, auth guard
95 KB /(app)/calendar Acceptable; rrule is shared
85 KB /(app)/todo Acceptable

Priority improvements

  1. /invoices/[id] code-split — 534 KB is large. swissqrbill and pdf-lib are probably both eagerly imported. Wrap the PDF/QR generation path with await import('swissqrbill') so the route bundle drops to ~150 KB for the list/display case. Real win for Swiss-bill users on 3G / slow laptops.

  2. @mana/shared-icons chunking — 317 + 149 KB of Phosphor SVG paths across two chunks. Phosphor doesn't tree-shake well because its icons are named exports of a single module. Either:

    • migrate to tree-shakable per-icon imports, OR
    • lazy-load rarely-used icons in the module that imports them instead of the shared chunk.
  3. Root (app) layout (260 KB) — on the high side for "just the shell". Investigate whether any module-specific stores / AI pipeline bits are leaking into the shared layout when they could be route-scoped.

What's already good

  • Entry bundle is 92 KB — no bloat in the critical path.
  • 22 MB of WASM (transformers.js) is correctly behind Web Workers — cold visitors don't pay for it.
  • zxcvbn (1.25 MB German dict + keyboard graphs) is correctly behind a dynamic import in PasswordStrength.svelte — only register / recovery / password-change surfaces load it.
  • Route-level code-splitting is working: 230 separate route bundles, median ~12 KB.

Usage

pnpm --filter @mana/web build          # prerequisite
pnpm run audit:bundle                   # full report
pnpm run audit:bundle --top 30          # show top N chunks
pnpm run audit:bundle --summary         # category totals only

Heuristic rules: chunks in chunks/ with ≥ 200 KB get a ⚠ flag. Route bundles (nodes/) and worker bundles (workers/) are always-lazy and don't get flagged. The content-hint regex knows about transformers.js, zxcvbn, tiptap, pdf-lib, swissqrbill, rrule, suncalc, dexie, marked, pako, date-fns, zod, svelte-dnd-action, svelte-i18n, WASM, Phosphor icons, and Vite's own mapDeps metadata.