perf(build): drop the JIT compiler from production, 1.55 MB -> 1.04 MB (FH-3.4)

Built with --stats-json and read the esbuild metafile instead of guessing.
@angular/compiler was 495,831 bytes of a 1.5 MB main chunk - the JIT
compiler, in an AOT production build, 33% of everything an anonymous
visitor downloads.

src/main.ts imported it deliberately, with a comment explaining that
@marketplaces/auth shipped plain tsc output carrying no Ivy metadata, so
Angular JIT-compiled its classes at runtime and bootstrap threw without
it.

That comment was stale. The package is 0.2.0, built with ng-packagr,
module: dist/fesm2022/marketplaces-auth.mjs - proper Angular Package
Format, partial-compiled (ɵɵngDeclareInjectable), linked at consumer
build time. Nothing needs JIT.

Verified against the production bundle served statically, not just a
green build: the failure mode this guarded was a runtime throw, so a
successful compile proves nothing. Angular 22.0.8 bootstrapped, the
router resolved /ru, and the app rendered its own "server unavailable"
screen - meaning DI, HttpClient and the full interceptor chain ran. That
chain injects AuthService from @marketplaces/auth, the exact class the
old comment named. Zero JIT or compiler errors; the only console output
was the expected 404s from having no backend behind a static server.

Initial bundle 1.55 MB -> 1.04 MB raw, 323.58 kB -> 215.45 kB transfer.
Budget ratchet lowered 1.6MB -> 1.1MB, which is now also what stops the
import being re-added.

Next lever, deliberately not taken here: i18n/ru.ts is 290 kB, eager,
while en/hy are already lazy. TranslateService has the loader plumbing
and languageGuard already awaits a preload, so it is mechanically small -
but it adds a round-trip before first paint for the majority language,
which is a product tradeoff, not a cleanup. Roughly a 750 kB bundle once
someone decides.

256 tests pass.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
sdarbinyan
2026-08-21 16:10:34 +04:00
parent a35953d90f
commit 771dce9e29
3 changed files with 40 additions and 20 deletions

View File

@@ -63,7 +63,7 @@
{
"type": "initial",
"maximumWarning": "700kB",
"maximumError": "1.6MB"
"maximumError": "1.1MB"
},
{
"type": "anyComponentStyle",

View File

@@ -131,13 +131,31 @@ Each item is normative text plus an acceptance scenario in `docs/backend/BACKEND
Their §22 scenario 10. The same provider event delivered twice does not complete the order twice or move stock twice.
- [x] **FH-3.3 — Bundle budget as a blocking CI check** · S · **done 2026-08-21**
`maximumError` on the initial bundle lowered `1.8MB → 1.6MB` in `angular.json`. Measured today: **1.55 MB raw / 324.58 kB transfer** — worse than the 1.15 MB they measured on 11 Aug, so this had been growing unwatched. The threshold is a **ratchet**, not the target: set just above today's size so the bundle cannot grow, with the 700 kB warning left in place as the goal. Lower it every time the number comes down. CI now runs the production build (`npm run build` already defaults to production).
`maximumError` on the initial bundle lowered `1.8MB → 1.6MB` in `angular.json`, and again to `1.1MB` once FH-3.4 landed. Measured today: **1.55 MB raw / 324.58 kB transfer** — worse than the 1.15 MB they measured on 11 Aug, so this had been growing unwatched. The threshold is a **ratchet**, not the target: set just above today's size so the bundle cannot grow, with the 700 kB warning left in place as the goal. Lower it every time the number comes down. CI now runs the production build (`npm run build` already defaults to production).
- [ ] **FH-3.4 — Get the initial bundle down** · L · `angular.json`, `src/app/`
**Premise corrected after measuring.** Admin, editor, catalog, cart and the `en`/`hy` locales are *already* lazy chunks — nothing admin-shaped ships to an anonymous visitor. The entire 1.55 MB is `main` alone. So this is not a "split the deployables" job; it is a "find what is eager" job.
Known contributors: `src/app/i18n/ru.ts` (141 kB of source, default locale, eager while `en`/`hy` are lazy), the eagerly-provided core services and their DI tokens, `icon-registry.ts`.
Also: `qrcode`, pulled in by `@marketplaces/auth`, is not ESM and causes an optimizer bailout — worth fixing in the package.
**Done when:** initial is under the 700 kB warning, with the ratchet lowered in steps along the way.
- [~] **FH-3.4 — Get the initial bundle down** · L · **1.55 MB → 1.04 MB on 2026-08-21; not yet at target**
**Premise corrected after measuring.** Admin, editor, catalog, cart and the `en`/`hy` locales are *already* lazy chunks — nothing admin-shaped ships to an anonymous visitor. The whole initial bundle is `main` alone, so this was never a "split the deployables" job.
Built with `--stats-json` and read the esbuild metafile rather than guessing. Composition of the 1.5 MB `main`:
| bytes | what |
|---:|---|
| 495,831 | `@angular/compiler` |
| 290,589 | `src/app/i18n/ru.ts` |
| 162,915 | `@angular/core` |
| 82,209 | `@angular/router` |
| 53,354 | `@angular/common` |
**`@angular/compiler` — 33% of the bundle — was the JIT compiler, in an AOT production build.** `src/main.ts` imported it explicitly, with a comment explaining that `@marketplaces/auth` shipped plain `tsc` output carrying no Ivy metadata, so Angular JIT-compiled its classes at runtime and bootstrap threw without it.
That comment was **stale**. The package is `0.2.0`, built with ng-packagr, `module: dist/fesm2022/marketplaces-auth.mjs` — proper Angular Package Format, partial-compiled (`ɵɵngDeclareInjectable`), linked at consumer build time. No JIT needed.
Removed the import. Verified against the **production** bundle served statically, not just a successful build — the failure mode it guarded was a runtime throw, so a green build proves nothing. Angular 22.0.8 bootstrapped, the router resolved `/ru`, and the app rendered its own "server unavailable" screen, which means DI, HttpClient and the whole interceptor chain ran. That chain injects `AuthService` from `@marketplaces/auth` — the exact class named in the old comment. Zero JIT/compiler errors; the only console output was the expected 404s from having no backend.
**Result: 1.55 MB → 1.04 MB raw, 323.58 kB → 215.45 kB transfer.** Ratchet lowered `1.6MB → 1.1MB`, which is now also the guard against the import being re-added.
**Remaining path to the 700 kB target.** The next lever is `i18n/ru.ts` at 290 kB — the default locale, eager, while `en`/`hy` are lazy. `TranslateService` already has the loader plumbing and `languageGuard` already awaits a preload before route activation, so making `ru` lazy is mechanically small. It is deliberately **not** done here: it adds a round-trip before first paint for the majority language, which is a product tradeoff rather than a cleanup. Needs a decision, then it is roughly a 750 kB bundle.
Also outstanding: `qrcode` (23.7 kB), pulled in by `@marketplaces/auth`, is not ESM and causes an optimizer bailout — a fix for the package repo.
- [x] **FH-3.5 — Bundle secret scan in CI** · S · **done 2026-08-21**
`scripts/ci/scan-bundle.sh`, wired as `npm run scan:bundle` and a CI step after Build. Seven patterns: both provider auth headers, the partner ID shape, `client_secret`, private key blocks, AWS keys, Telegram bot tokens. Verified in both directions — clean against the real `dist/`, and fails with exit 1 against a planted credential.
@@ -227,7 +245,10 @@ The **client half and the contract are done** (2026-08-21). What remains is back
- [x] **FH-E.4 — ADR for the harvest** · S · **done 2026-08-21**
[ADR-0006](context/adrs/ADR-0006-harvest-mechanisms-from-the-parallel-platform.md). Records what we take, what we reject, what we keep because ours is better, and the one organizational question it deliberately does not settle.
- [ ] **FH-E.6 — Keep mock gateways out of production builds** · M · Lane A
- [x] **FH-E.6 — Keep mock gateways out of production builds** · M · **done 2026-08-21**
21 token factories now `inject(XApiGateway)` unconditionally; mock overrides moved to `src/app/mock-gateway.providers.ts`, swapped for a production copy that imports nothing via `fileReplacements`. Zero `*LocalGateway` classes and zero fixtures in the production bundle, down from 21 classes and 75 kB of source. `scan-bundle.sh` gained two patterns so it cannot return, verified in both directions. Dev behaviour unchanged — flip `useMockData` in `environment.ts` as before.
Worth noting for whoever picks up FH-E.5: `useMockData` is `false` in **both** environment files, so none of these mocks were ever the selected implementation. They were pure weight.
**Not fixed here:** `MediaRepository` is still bound to `MockMediaRepository` unconditionally in `app.config.ts`. That one cannot simply be deleted — no real implementation exists — so it is a missing API gateway, not dead weight.
Measured 2026-08-21: mock seed data reaches the production bundle. `ptr_local`, a fixture literal from `partner-hierarchy-local.gateway.ts`, is present in a built lazy chunk. Cause: 21 DI tokens use `factory: () => (environment.useMockData ? inject(XLocalGateway) : inject(XApiGateway))`, and referencing both branches keeps both classes reachable, so the optimizer cannot drop the mock. 75 kB of local-gateway source, plus its fixtures, ships to users.
This is the concrete form of their strongest objection — "mock repositories as production implementation" — and it is mechanical to fix. The pattern to copy is already in this repo: `mock-data.interceptor.production.ts` swapped in via `fileReplacements`.
**Done when:** `scan-bundle.sh` can gate on mock fixture markers and pass.
@@ -246,11 +267,11 @@ The **client half and the contract are done** (2026-08-21). What remains is back
| 0 — Decide | 0 | — | 2 | needs a person, not a session |
| 1 — Live defects | 2 | — | 2 | FH-1.2 / FH-1.4 sit in files another session owns |
| 2 — Contracts | 14 | — | 0 | 1 rejected (FH-2.12) |
| 3 — Proof | 2 | | 3 | see note below |
| 3 — Proof | 3 | 1 | 1 | see note below |
| 4 — Identity | 2 | 3 | 3 | OAuth apps, which FH-0.1 gates |
| Ops | 1 | — | 2 | — |
| Process | 4 | — | 2 | — |
| **Total** | **25** | **3** | **14** | 1 rejected |
| Process | 5 | — | 1 | — |
| **Total** | **27** | **4** | **11** | 1 rejected |
**Landed 2026-08-21**
@@ -260,7 +281,7 @@ The **client half and the contract are done** (2026-08-21). What remains is back
- **Wave 4** — FH-4.1 and FH-4.2 complete on the client and in the contract; FH-4.34.5 specified and waiting on backend plus registered OAuth applications.
- **Process** — FH-E.1E.4, including [ADR-0006](context/adrs/ADR-0006-harvest-mechanisms-from-the-parallel-platform.md).
Test count over the session: 247 256 (+4 geo, +5 social identity). Build green, boundary and cycle checks green.
Test count over the session: 247 -> 256 (+4 geo, +5 social identity). Initial bundle 1.55 MB -> 1.04 MB. Build green, boundary and cycle checks green.
**On FH-3.1 / FH-3.2 — reclassified, not skipped**

View File

@@ -1,15 +1,14 @@
import 'zone.js';
// @marketplaces/auth ships plain tsc output (dist/index.js), not Angular
// Package Format - its classes carry no compiled Ivy DI metadata, so Angular
// falls back to JIT-compiling them at runtime. Without this import that
// throws immediately on first injection (AuthService, Ed25519 services, any
// class from the package), which breaks app bootstrap and admin auth
// entirely. Real fix: publish the package via ng-packagr. Tracked - do not
// remove this import until that ships.
// No '@angular/compiler' import here, deliberately. It was needed while
// @marketplaces/auth shipped plain tsc output with no Ivy metadata, forcing
// Angular to JIT-compile the package's classes at runtime. The package is
// published via ng-packagr as of 0.2.0 - partial-compiled, linked at build
// time - so the JIT compiler is dead weight: 496 kB of a 1.5 MB initial
// bundle. Re-adding it will blow the bundle budget in CI, which is the
// intended guard.
import { bootstrapApplication } from '@angular/platform-browser';
import { appConfig } from './app/app.config';
import { App } from './app/app';
import '@angular/compiler';
bootstrapApplication(App, appConfig)
.catch((err) => console.error(err));