mirror of
https://github.com/Memo-2023/mana-monorepo.git
synced 2026-05-15 00:01:10 +02:00
/(app)/invoices/[id] route bundle drops from **534 KB → 18.6 KB** by
moving PDF rendering behind dynamic imports.
Changes:
- views/DetailView.svelte: `await import('../pdf/renderer')` inside
renderPdf() + downloadPdf(), cached in a module-local ref.
- components/SendModal.svelte: same for openAndDownload().
- pdf/scor.ts (new): generateSCORReference extracted so the
invoices store can derive a reference string without pulling
swissqrbill/svg + pdf-lib into the list-view bundle.
- pdf/qr-bill.ts: re-exports generateSCORReference from scor.ts
for backward compatibility.
- stores/invoices.svelte.ts: imports from ../pdf/scor (light) instead
of ../pdf/qr-bill (heavy).
- index.ts: drop re-export of the PDF renderer from the module
barrel so `import ... from '$lib/modules/invoices'` never drags
pdf-lib in.
The heavy chunk (pdf-lib + swissqrbill, ~576 KB) now only loads when
a user actually opens an invoice detail — list views, create flow, and
all other routes stay lean.
20/20 qr-bill tests pass; svelte-check clean.
Bonus: scripts/audit-icon-usage.mjs (+ pnpm run audit:icon-usage)
audits @mana/shared-icons imports. Reveals 204 distinct icons across
the codebase, 199 of them at default weight but paying for all 6
Phosphor weights. Biggest offender: app-registry/apps.ts with 69
static icon imports accounting for ~290 KB of the shared 466 KB icon
chunk. Migration path for that is documented in
docs/optimizable/bundle-analysis.md §2 — next session's work.
docs/optimizable/bundle-analysis.md also updated with the root (app)
layout (260 KB) investigation notes (start/stop lifecycle hooks to
defer via idleCallback).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
125 lines
5.7 KiB
Markdown
125 lines
5.7 KiB
Markdown
# 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 — ✅ **SHIPPED 2026-04-22**
|
||
|
||
**Before:** 534 KB route bundle. **After:** 18.6 KB.
|
||
|
||
`DetailView.svelte` + `SendModal.svelte` now import `./pdf/renderer`
|
||
dynamically (`await import(...)`) so pdf-lib + swissqrbill/svg (~576 KB
|
||
combined) move into a separate chunk that only loads when the user
|
||
actually opens an invoice detail. Also split `generateSCORReference`
|
||
into its own `./pdf/scor.ts` so the invoices store can compute a
|
||
reference on create without pulling the heavy renderer graph.
|
||
|
||
### 2. `@mana/shared-icons` — **OPEN**
|
||
|
||
466 KB of Phosphor SVG paths across 2 chunks. Root cause from
|
||
`audit:icon-usage` report (2026-04-22):
|
||
|
||
- **204 distinct icons** imported across the codebase.
|
||
- **199 use the default "regular" weight** — but Phosphor ships all
|
||
6 weights per icon regardless.
|
||
- Single worst offender: `app-registry/apps.ts` imports **69 icons**
|
||
in one file (the module-name → icon-component map), pulled into the
|
||
shared layout chunk → 69 × 6 weights × ~0.7 KB ≈ 290 KB on every
|
||
cold load.
|
||
|
||
**Migration paths** (pick one, sized to follow-up sessions):
|
||
|
||
1. Rewrite `app-registry/apps.ts` so each module's icon is a string
|
||
name, with a lazy `getIconComponent(name)` helper backed by
|
||
per-path dynamic imports (`() => import('phosphor-svelte/House')`).
|
||
Saves ~290 KB from the initial layout chunk. Biggest single win.
|
||
2. Drop `export * from 'phosphor-svelte'` in
|
||
`packages/shared-icons/src/index.ts` and re-export only the 204
|
||
icons actually used. Defends against future barrel-broadening.
|
||
3. Longest-term: build a custom icon set that only ships the weights
|
||
actually used (most icons only need "regular").
|
||
|
||
Run `pnpm run audit:icon-usage --top 30` for the current inventory.
|
||
|
||
### 3. Root `(app)` layout (260 KB) — **OPEN**
|
||
|
||
`routes/(app)/+layout.svelte` statically imports ~15 start/stop
|
||
lifecycle hooks (mission tick, server-iteration executor, event store,
|
||
event bridge, streak tracker, goal tracker, byok init, tools init,
|
||
articles-from-news migration, reminder scheduler, llm queue). Each
|
||
pulls its own dependency graph into the shared layout chunk.
|
||
|
||
**Recommended approach:** wrap the non-critical ones in `queueMicrotask`
|
||
or `requestIdleCallback`-deferred dynamic imports — the layout finishes
|
||
hydrating, then the heavy lifecycle code streams in. The one-shot
|
||
`runArticlesFromNewsMigration` in particular is a prime candidate since
|
||
it's executed only once per user per session.
|
||
|
||
## 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
|
||
|
||
```bash
|
||
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.
|