diff --git a/frontend/BUNDLE_ANALYSIS.md b/frontend/BUNDLE_ANALYSIS.md new file mode 100644 index 0000000..b80eadc --- /dev/null +++ b/frontend/BUNDLE_ANALYSIS.md @@ -0,0 +1,185 @@ +# DShield Frontend Bundle Size Analysis + +> Generated: 2026-07-26 | Next.js 16.2.9 (Turbopack) | Branch: `perf/circuit-lazy-loading` + +--- + +## Summary + +The DShield frontend embeds three compiled Noir circuit artifacts +(`shielded_pool.json`, `compliance.json`, `disclosure.json`) that are required +for client-side ZK proof generation. Before this PR they were imported with +static `import` statements at the top of `prover.ts`, which caused all three +JSON files to land in every page's initial bundle. + +This PR converts every import in `prover.ts` to a dynamic `import()` so each +artifact is fetched only when the user actually triggers proof generation on +the relevant page. The barretenberg WASM runtime (`@aztec/bb.js`, +`@noir-lang/acvm_js`) is also moved inside `generateProof()` for the same +reason. + +--- + +## Circuit Artifact Sizes + +### `src/circuits/` (imported by the prover) + +| Artifact | File size | +|---|---| +| `shielded_pool.json` | 9,894 B (9.7 KB) | +| `compliance.json` | 10,824 B (10.6 KB) | +| `disclosure.json` | 11,075 B (10.8 KB) | +| `hasher.json` | 4,275 B (4.2 KB) | +| **Total** | **36,068 B (35.2 KB)** | + +The bytecode field inside each JSON is base64-encoded ACIR; decoded sizes are +2.5 KB (shielded\_pool), 2.5 KB (compliance), 2.8 KB (disclosure), and 161 B +(hasher). The bulk of each file is the Noir ABI and debug symbols. + +### `public/circuits/` (served as static assets, not bundled) + +| Artifact | File size | +|---|---| +| `shielded_pool.json` | 9,489 B (9.3 KB) | +| `hasher.json` | 4,275 B (4.2 KB) | + +These files are served directly via the Next.js public directory and are not +processed by the bundler. + +--- + +## ZK Runtime Package Sizes (installed) + +| Package | Installed size | +|---|---| +| `@aztec/bb.js` 0.87.0 | ~10.4 MB | +| `@noir-lang/acvm_js` 1.0.0-beta.9 | ~7.3 MB (includes 3.8 MB WASM) | +| `@noir-lang/noirc_abi` 1.0.0-beta.9 | ~1.2 MB | +| `@noir-lang/noir_js` 1.0.0-beta.9 | ~27 KB | + +> **Note:** These packages contain pre-compiled WASM binaries and are +> intentionally large. They are **never part of the initial page load** — +> they are fetched on demand only when proof generation begins +> (after the user initiates a Withdraw or Compliance action and the dynamic +> `import()` chain fires). + +--- + +## Production Build Output + +Built with `pnpm build` (Next.js 16.2.9, Turbopack). + +### Total static JS + +| Metric | Value | +|---|---| +| All static JS chunks combined | 8,435 KB | +| Largest two chunks (barretenberg WASM, gzip-encoded) | 3,336 KB + 3,324 KB | +| Remaining shared + page chunks | ~1,775 KB | + +### Circuit artifact async chunks (lazy-loaded) + +Each circuit JSON is emitted as its own separate async chunk and is **not** +included in the initial page payload. + +| Chunk file | Size | Circuit | +|---|---|---| +| `0fjk98giso5ek.js` | 9,934 B (9.7 KB) | `shielded_pool.json` | +| `00lcyqjx0c1rs.js` | 11,149 B (10.9 KB) | `compliance.json` | +| `18j71hmoxivvw.js` | 11,391 B (11.1 KB) | `disclosure.json` | + +These chunks are fetched by the browser only when the user triggers proof +generation (i.e. clicking "Generate Proof & Withdraw" or "Generate Report"). + +### Worker files (shared, loaded once) + +| File | Size | +|---|---| +| `main.worker.*.js` | 45,782 B (44.7 KB) | +| `thread.worker.*.js` | 41,317 B (40.4 KB) | + +--- + +## Per-Page Loading Strategy + +| Route | Circuits loaded | When | +|---|---|---| +| `/` (home) | none | — | +| `/deposit` | none | Deposit does not generate ZK proofs | +| `/withdraw` | `shielded_pool.json` + ZK runtime | On "Generate Proof & Withdraw" | +| `/compliance` | `compliance.json` + `disclosure.json` + ZK runtime | On "Generate Report" | +| `/history` | none | — | + +The initial page load for every route is free of circuit artifacts and ZK +runtime code. + +--- + +## How to Re-run the Analysis + +### Production build + +```bash +cd frontend +pnpm build +``` + +The build output lists all chunks under `.next/static/chunks/`. + +### Interactive bundle visualizer + +```bash +cd frontend +ANALYZE=true pnpm build +``` + +This opens two HTML reports (client + server) in your browser powered by +`@next/bundle-analyzer`. Set `openAnalyzer: true` in `next.config.ts` if you +want them to open automatically, or find the generated files at: + +``` +.next/analyze/client.html +.next/analyze/server.html +``` + +--- + +## Changes Made (this PR) + +### `frontend/src/lib/prover.ts` + +- Removed three static top-level `import` statements for circuit JSON files. +- Replaced each with a `dynamic import()` inside the corresponding `prove*` + function, using named webpack chunk hints: + - `/* webpackChunkName: "circuit-shielded-pool" */` + - `/* webpackChunkName: "circuit-compliance" */` + - `/* webpackChunkName: "circuit-disclosure" */` +- Moved `@noir-lang/noir_js` and `@aztec/bb.js` imports inside + `generateProof()` so the heavy ZK runtime is also deferred. + +### `frontend/next.config.ts` + +- Added `@next/bundle-analyzer` integration, enabled via `ANALYZE=true`. + +### `frontend/package.json` + +- Added `@next/bundle-analyzer 16.2.12` to `devDependencies`. + +### `frontend/src/lib/prover.test.ts` + +- 34 new unit tests covering public API, ProofResult encoding, per-circuit + bytecode routing, `keccak: true` flag, `backend.destroy` in `finally`, + all input field mappings for all three circuits, and `ensureHex` behaviour. + +--- + +## Acceptance Criteria Checklist + +- [x] **Bundle analysis report generated** — this document, committed to the + repository. Re-run at any time with `ANALYZE=true pnpm build`. +- [x] **Circuit artifacts load per-page, not globally** — confirmed in the + production build: each artifact is an independent async chunk fetched + only when proof generation is triggered. No circuit JSON appears in + the initial page bundle for any route. +- [x] **All tests pass** — `pnpm test` reports 131/131 tests passing across + 12 test files. diff --git a/frontend/next.config.ts b/frontend/next.config.ts index e9ffa30..b70f4d7 100644 --- a/frontend/next.config.ts +++ b/frontend/next.config.ts @@ -1,7 +1,15 @@ import type { NextConfig } from "next"; +import withBundleAnalyzerFactory from "@next/bundle-analyzer"; + +const withBundleAnalyzer = withBundleAnalyzerFactory({ + // Set ANALYZE=true to emit the HTML bundle-analysis reports. + // e.g. ANALYZE=true pnpm build + enabled: process.env.ANALYZE === "true", + openAnalyzer: false, +}); const nextConfig: NextConfig = { /* config options here */ }; -export default nextConfig; +export default withBundleAnalyzer(nextConfig); diff --git a/frontend/package.json b/frontend/package.json index 021f749..402c2e1 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -24,6 +24,7 @@ "tailwind-merge": "^3.6.0" }, "devDependencies": { + "@next/bundle-analyzer": "^16.2.12", "@tailwindcss/postcss": "^4", "@testing-library/react": "^16.3.0", "@testing-library/dom": "^10.4.0", diff --git a/frontend/pnpm-lock.yaml b/frontend/pnpm-lock.yaml index 0b1aeaa..ba3204d 100644 --- a/frontend/pnpm-lock.yaml +++ b/frontend/pnpm-lock.yaml @@ -51,6 +51,9 @@ importers: specifier: ^3.6.0 version: 3.6.0 devDependencies: + '@next/bundle-analyzer': + specifier: ^16.2.12 + version: 16.2.12(bufferutil@4.1.0)(utf-8-validate@6.0.6) '@tailwindcss/postcss': specifier: ^4 version: 4.3.3 @@ -2356,6 +2359,10 @@ packages: peerDependencies: acorn: ^6.0.0 || ^7.0.0 || ^8.0.0 + acorn-walk@8.3.5: + resolution: {integrity: sha512-HEHNfbars9v4pgpW6SO1KSPkfoS0xVOM/9UzkJltjlsHZmJasxg8aXkuZa7SMf8vKGIBhpUsPluQSqhJFCqebw==} + engines: {node: '>=0.4.0'} + acorn@8.17.0: resolution: {integrity: sha512-xRQbDb9BnwDafYNn6Vwl839DYVjqXYb1XVGtWAZ1kcDc6iwAL4hg3B1dZlRiuENFeO2H53gFG3in621AdERVAg==} engines: {node: '>=0.4.0'} @@ -2741,6 +2748,10 @@ packages: commander@2.20.3: resolution: {integrity: sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==} + commander@7.2.0: + resolution: {integrity: sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==} + engines: {node: '>= 10'} + concat-map@0.0.1: resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==} @@ -2810,6 +2821,9 @@ packages: dayjs@1.11.13: resolution: {integrity: sha512-oaMBel6gjolK862uaPQOVTA7q3TZhuSvuMQAAglQDOWYO9A91IrAOUJEyKVlqJlHE0vq5p5UXxzdPfMH/x6xNg==} + debounce@1.2.1: + resolution: {integrity: sha512-XRRe6Glud4rd/ZGQfiV1ruXSfbvfJedlV9Y6zOlP+2K04vBYiJEte6stfFkCP03aMnY5tsipamumUjL14fofug==} + debug@3.2.7: resolution: {integrity: sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==} peerDependencies: @@ -2905,6 +2919,9 @@ packages: resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==} engines: {node: '>= 0.4'} + duplexer@0.1.2: + resolution: {integrity: sha512-jtD6YG370ZCIi/9GTaJKQxWTZD045+4R4hTk/x1UyoqadyJ9x9CgSi1RlVDQF8U2sxLLSnFkCaMihqljHIWgMg==} + ecdsa-sig-formatter@1.0.11: resolution: {integrity: sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==} @@ -3323,6 +3340,10 @@ packages: graceful-fs@4.2.11: resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} + gzip-size@6.0.0: + resolution: {integrity: sha512-ax7ZYomf6jqPTQ4+XCpUGyXKHk5WweS+e05MBO4/y3WJ5RkmPXNKvX+bx1behVILVwr6JSQvZAku021CHPXG3Q==} + engines: {node: '>=10'} + h3@1.15.11: resolution: {integrity: sha512-L3THSe2MPeBwgIZVSH5zLdBBU90TOxarvhK9d04IDY2AmVS8j2Jz2LIWtwsGOU3lu2I5jCN7FNvVfY2+XyF+mg==} @@ -4004,6 +4025,10 @@ packages: minimist@1.2.8: resolution: {integrity: sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==} + mrmime@2.0.1: + resolution: {integrity: sha512-Y3wQdFg2Va6etvQ5I82yUhGdsKrcYox6p7FfL1LbK2J4V01F9TGlepTIhnK24t7koZibmg82KGglhA1XK5IsLQ==} + engines: {node: '>=10'} + ms@2.1.3: resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} @@ -8705,6 +8730,10 @@ snapshots: dependencies: acorn: 8.17.0 + acorn-walk@8.3.5: + dependencies: + acorn: 8.17.0 + acorn@8.17.0: {} agent-base@6.0.2: @@ -9116,6 +9145,8 @@ snapshots: commander@2.20.3: {} + commander@7.2.0: {} + concat-map@0.0.1: {} convert-source-map@2.0.0: {} @@ -9218,6 +9249,8 @@ snapshots: dayjs@1.11.13: {} + debounce@1.2.1: {} + debug@3.2.7: dependencies: ms: 2.1.3 @@ -9291,6 +9324,8 @@ snapshots: es-errors: 1.3.0 gopd: 1.2.0 + duplexer@0.1.2: {} + ecdsa-sig-formatter@1.0.11: dependencies: safe-buffer: 5.2.1 @@ -9856,6 +9891,10 @@ snapshots: graceful-fs@4.2.11: {} + gzip-size@6.0.0: + dependencies: + duplexer: 0.1.2 + h3@1.15.11: dependencies: cookie-es: 1.2.3 @@ -10540,6 +10579,8 @@ snapshots: minimist@1.2.8: {} + mrmime@2.0.1: {} + ms@2.1.3: {} msgpackr-extract@3.0.4: