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:
@@ -63,7 +63,7 @@
|
|||||||
{
|
{
|
||||||
"type": "initial",
|
"type": "initial",
|
||||||
"maximumWarning": "700kB",
|
"maximumWarning": "700kB",
|
||||||
"maximumError": "1.6MB"
|
"maximumError": "1.1MB"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"type": "anyComponentStyle",
|
"type": "anyComponentStyle",
|
||||||
|
|||||||
@@ -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.
|
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**
|
- [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/`
|
- [~] **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 entire 1.55 MB is `main` alone. So this is not a "split the deployables" job; it is a "find what is eager" job.
|
**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.
|
||||||
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.
|
Built with `--stats-json` and read the esbuild metafile rather than guessing. Composition of the 1.5 MB `main`:
|
||||||
**Done when:** initial is under the 700 kB warning, with the ratchet lowered in steps along the way.
|
|
||||||
|
| 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**
|
- [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.
|
`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**
|
- [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.
|
[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.
|
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`.
|
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.
|
**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 |
|
| 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 |
|
| 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) |
|
| 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 |
|
| 4 — Identity | 2 | 3 | 3 | OAuth apps, which FH-0.1 gates |
|
||||||
| Ops | 1 | — | 2 | — |
|
| Ops | 1 | — | 2 | — |
|
||||||
| Process | 4 | — | 2 | — |
|
| Process | 5 | — | 1 | — |
|
||||||
| **Total** | **25** | **3** | **14** | 1 rejected |
|
| **Total** | **27** | **4** | **11** | 1 rejected |
|
||||||
|
|
||||||
**Landed 2026-08-21**
|
**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.3–4.5 specified and waiting on backend plus registered OAuth applications.
|
- **Wave 4** — FH-4.1 and FH-4.2 complete on the client and in the contract; FH-4.3–4.5 specified and waiting on backend plus registered OAuth applications.
|
||||||
- **Process** — FH-E.1–E.4, including [ADR-0006](context/adrs/ADR-0006-harvest-mechanisms-from-the-parallel-platform.md).
|
- **Process** — FH-E.1–E.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**
|
**On FH-3.1 / FH-3.2 — reclassified, not skipped**
|
||||||
|
|
||||||
|
|||||||
15
src/main.ts
15
src/main.ts
@@ -1,15 +1,14 @@
|
|||||||
import 'zone.js';
|
import 'zone.js';
|
||||||
// @marketplaces/auth ships plain tsc output (dist/index.js), not Angular
|
// No '@angular/compiler' import here, deliberately. It was needed while
|
||||||
// Package Format - its classes carry no compiled Ivy DI metadata, so Angular
|
// @marketplaces/auth shipped plain tsc output with no Ivy metadata, forcing
|
||||||
// falls back to JIT-compiling them at runtime. Without this import that
|
// Angular to JIT-compile the package's classes at runtime. The package is
|
||||||
// throws immediately on first injection (AuthService, Ed25519 services, any
|
// published via ng-packagr as of 0.2.0 - partial-compiled, linked at build
|
||||||
// class from the package), which breaks app bootstrap and admin auth
|
// time - so the JIT compiler is dead weight: 496 kB of a 1.5 MB initial
|
||||||
// entirely. Real fix: publish the package via ng-packagr. Tracked - do not
|
// bundle. Re-adding it will blow the bundle budget in CI, which is the
|
||||||
// remove this import until that ships.
|
// intended guard.
|
||||||
import { bootstrapApplication } from '@angular/platform-browser';
|
import { bootstrapApplication } from '@angular/platform-browser';
|
||||||
import { appConfig } from './app/app.config';
|
import { appConfig } from './app/app.config';
|
||||||
import { App } from './app/app';
|
import { App } from './app/app';
|
||||||
import '@angular/compiler';
|
|
||||||
|
|
||||||
bootstrapApplication(App, appConfig)
|
bootstrapApplication(App, appConfig)
|
||||||
.catch((err) => console.error(err));
|
.catch((err) => console.error(err));
|
||||||
|
|||||||
Reference in New Issue
Block a user