docs: add ADR-0017 for performance budgets (Core Web Vitals + Lighthouse CI + Angular budgets + BFF SLOs)
Pin the perf framework. Front-end metrics tracked at Google Core Web Vitals 'Good' thresholds (LCP <= 2.5s, INP <= 200ms, CLS <= 0.1, plus TBT <= 200ms, TTFB <= 800ms) and Lighthouse Performance >= 90 on critical routes. Lighthouse CI (@lhci/cli) enforces them in CI with median-of-3 runs to mitigate runner variance, on a curated critical-routes list (login, home, accessibility statement, flagship features as they land). Wires into the perf gate slot from ADR-0015. Bundle budgets enforced at nx build via Angular's project.json budgets array, type 'error': initial <= 300 KB gzip, lazy chunks <= 100 KB gzip, per-component CSS <= 6 KB. source-map-explorer wired as an Nx analyze target for diagnosis on budget breaches. Back-end SLOs documented per endpoint family (p95/p99) and observed via the OpenTelemetry spans already shipped by ADR-0012 - advisory in CI (load profile unrepresentative), alerting in production. Quarterly review tightens budgets when achievable. Scheduled weekly Lighthouse run on the prod env via the existing security-scheduled.yml workflow extends coverage beyond PR-time. Explicit a11y/perf trade-off rule: when they conflict, a11y wins (per APF's mission, ADR-0016). The perf budget is then re-evaluated at the next quarterly review. No browser-side RUM SDK in v1 - the OTel browser tracing from ADR-0012 plus scheduled prod Lighthouse runs cover the gap. RUM revisited in v2 if a real incident escapes the existing signal. decisions/README.md index updated. CLAUDE.md gains an explicit 'Performance budgets' line pointing to ADR-0017.
This commit is contained in:
@@ -45,6 +45,7 @@ The structural choices are recorded as ADRs and summarized below. Any change to
|
||||
- **Downstream API access:** unified `DownstreamApiClient` (`@nestjs/axios` + `cockatiel`), per-service `DownstreamApiConfig`; default auth strategy is **OBO via MSAL Node** for Entra-protected APIs (downstream-scoped tokens cached in Redis with AES-256-GCM under a dedicated key); fallback strategy is service credential + signed `X-User-Assertion` JWT (BFF JWKS at `/.well-known/jwks.json`); per-call audience pre-check; no `axios`/`fetch` outside `src/downstream/` — see [ADR-0014](decisions/0014-downstream-api-access-obo-pattern.md).
|
||||
- **CI/CD:** **Gitea Actions** (level-2 implementation; will be superseded by a GitLab migration ADR within 6-18 months). Trunk-based with squash-merge, branch protection on `main`, all CI gates blocking. Thin YAML — orchestration logic lives in `package.json` scripts (`ci:check`, `ci:scan`, `ci:commits`) and Nx targets, runnable locally. Gates: format / lint / type-check / test / build / audit / secret-scan / commit-lint, plus `a11y` (per ADR-0016) and future `perf`. Self-hosted `act_runner` on-prem. Conventional Commits validated locally (hook) and in CI (defense in depth). Required reviewer count = 0 in v1, raised to ≥1 once a second contributor joins. Signed commits recommended, revisited at GitLab migration — see [ADR-0015](decisions/0015-cicd-gitea-actions.md).
|
||||
- **Accessibility:** **WCAG 2.2 AA baseline + targeted AAA** on criteria with high impact for APF's user base (1.4.6 Contrast Enhanced, 2.2.3 No Timing, 2.3.3 Animation, 3.1.5 Reading Level, 1.4.8 Visual Presentation, 2.4.9 Link Purpose, 3.3.5 Help). RGAA 4.1 alignment for French audit. UI stack: **Angular CDK + spartan-ng + Tailwind** (no Angular Material; no React libs). User-preferences panel (contrast / text size / motion / spacing / cognitive simplification / reading focus) persisted in session. Tooling: `@angular-eslint/template/*` lint, `@axe-core/playwright` e2e (blocking on critical/serious), token-contrast CI check, touch-target check (44×44 min). Manual testing cadence with APF's internal user panel before each major release. Public accessibility statement page at `/accessibility` and `/accessibilite` — see [ADR-0016](decisions/0016-accessibility-baseline-wcag-aa-targeted-aaa.md).
|
||||
- **Performance budgets:** Core Web Vitals at Google "Good" thresholds (LCP ≤ 2.5 s, INP ≤ 200 ms, CLS ≤ 0.1, TBT ≤ 200 ms, TTFB ≤ 800 ms), Lighthouse Performance ≥ 90 on critical routes. **Lighthouse CI** (`@lhci/cli`) runs in CI with median-of-3 mitigation, blocking on threshold breach. Angular bundle `budgets` (`type: "error"`): initial ≤ 300 KB gzip, lazy chunks ≤ 100 KB gzip. BFF p95/p99 SLOs per endpoint family observed via OTel (advisory in CI, alerting in prod). Weekly scheduled Lighthouse run on prod env. **a11y wins over perf** when they conflict — see [ADR-0017](decisions/0017-performance-budgets-lighthouse-ci.md).
|
||||
- **Local quality gates:** Husky + lint-staged + commitlint with Conventional Commits — see [ADR-0007](decisions/0007-pre-commit-hooks-and-conventional-commits.md).
|
||||
- **Runtime:** Node.js latest LTS major.
|
||||
|
||||
|
||||
@@ -0,0 +1,260 @@
|
||||
---
|
||||
status: accepted
|
||||
date: 2026-04-30
|
||||
decision-makers: R&D Lead
|
||||
tags: [performance, frontend, backend, process]
|
||||
---
|
||||
|
||||
# Performance budgets — Core Web Vitals + Lighthouse CI gates, bundle budgets, BFF p95/p99 SLOs
|
||||
|
||||
## Context and Problem Statement
|
||||
|
||||
The portal is a CSR Angular SPA ([ADR-0004](0004-frontend-stack-angular-csr-zoneless-signals.md)) — without SSR, perceived performance depends entirely on the JS payload, the rendering path, and the BFF response latency. The host organisation context ([ADR-0016](0016-accessibility-baseline-wcag-aa-targeted-aaa.md)) elevates performance further: users on assistive technologies (screen readers, switch controls, eye tracking) are particularly affected by slow or jumpy interfaces; cognitive-disability tolerance for delayed feedback is reduced.
|
||||
|
||||
We need to fix:
|
||||
- which metrics we track;
|
||||
- which thresholds bound them;
|
||||
- which tooling enforces them in CI and in production;
|
||||
- which gates block merges vs. surface warnings;
|
||||
- how BFF latency is bounded and observed.
|
||||
|
||||
This ADR fixes the framework. Concrete optimisation work happens at feature delivery time; this ADR ensures the bar exists and is enforced.
|
||||
|
||||
## Decision Drivers
|
||||
|
||||
* CSR-only ([ADR-0004](0004-frontend-stack-angular-csr-zoneless-signals.md)) — perceived perf is entirely client-side load + execution.
|
||||
* a11y context ([ADR-0016](0016-accessibility-baseline-wcag-aa-targeted-aaa.md)) — perf failure modes hurt the APF user base disproportionately.
|
||||
* Industry-standard tooling, anti-bricolage.
|
||||
* Same observability stack as [ADR-0012](0012-observability-pino-opentelemetry.md) — no parallel telemetry path.
|
||||
* Cohérence with [ADR-0015](0015-cicd-gitea-actions.md) — the `perf` gate slot was reserved for this ADR.
|
||||
|
||||
## Considered Options
|
||||
|
||||
### Metrics scope
|
||||
* **Google Core Web Vitals (LCP, INP, CLS) + supplementary (TBT, TTFB) + bundle size.** (Chosen.)
|
||||
* Custom metrics only (rejected — premature, not benchmarkable across the industry).
|
||||
* Lighthouse Performance score only (insufficient on its own — score-as-only-metric is gameable).
|
||||
|
||||
### Front-end tooling
|
||||
* **Lighthouse CI (`@lhci/cli`)** for full audit + score, plus Angular `budgets` for bundle size enforcement at build. (Chosen.)
|
||||
* WebPageTest API.
|
||||
* Calibre, Speedlify (à la pointe but less mature for CI integration).
|
||||
|
||||
### Threshold values
|
||||
* **Google "Good" Core Web Vitals thresholds + Lighthouse Performance ≥ 90.** (Chosen.)
|
||||
* Stricter (e.g. LCP ≤ 2 s, score ≥ 95) — rejected as too flaky in CI runners.
|
||||
* Looser (matches "Needs Improvement") — rejected as insufficient.
|
||||
|
||||
### Bundle budgets
|
||||
* **Angular `budgets` in `project.json`, type `error`, blocking the build on overshoot.** (Chosen.)
|
||||
* No budget (rejected — invariably leads to silent bloat).
|
||||
|
||||
### Back-end SLOs
|
||||
* **Per-endpoint-family p95/p99 budgets, observed via OTel spans (already in place by [ADR-0012](0012-observability-pino-opentelemetry.md)), enforced at review and in scheduled reports.** (Chosen.)
|
||||
* No back-end perf budget.
|
||||
|
||||
### Real-user monitoring (RUM)
|
||||
* **None in v1** — rely on OTel server-side spans + Lighthouse CI in CI + scheduled prod Lighthouse runs. (Chosen.)
|
||||
* Front-side RUM SDK (Sentry, Datadog Browser, custom OTel-Web RUM).
|
||||
|
||||
## Decision Outcome
|
||||
|
||||
### Metrics and thresholds (front-end)
|
||||
|
||||
Core Web Vitals — Google "Good" thresholds, measured by Lighthouse CI:
|
||||
|
||||
| Metric | Threshold | Source |
|
||||
| --- | --- | --- |
|
||||
| **LCP** (Largest Contentful Paint) | ≤ **2.5 s** | https://web.dev/lcp/ |
|
||||
| **INP** (Interaction to Next Paint) | ≤ **200 ms** | https://web.dev/inp/ — replaces FID since March 2024 |
|
||||
| **CLS** (Cumulative Layout Shift) | ≤ **0.1** | https://web.dev/cls/ |
|
||||
| **TBT** (Total Blocking Time, lab proxy of INP) | ≤ **200 ms** | https://web.dev/tbt/ |
|
||||
| **TTFB** (Time to First Byte) | ≤ **800 ms** | https://web.dev/ttfb/ |
|
||||
| **Lighthouse Performance score** | ≥ **90** on critical routes | Lighthouse 12+ |
|
||||
|
||||
Bundle budgets (`apps/portal-shell/project.json`, Angular `budgets` array, `type: "error"` — blocking at `nx build`):
|
||||
|
||||
| Bundle | Budget |
|
||||
| --- | --- |
|
||||
| Initial bundle (gzip) | ≤ **300 KB** |
|
||||
| Any lazy chunk (gzip) | ≤ **100 KB** |
|
||||
| Per-component CSS | ≤ **6 KB** |
|
||||
| Total stylesheet | ≤ **150 KB** |
|
||||
|
||||
These values are deliberately conservative for a zoneless + Signals Angular setup, which is compact by construction. Actual measurements may show we have headroom; the budget can be tightened (never loosened without ADR amendment) at quarterly review.
|
||||
|
||||
### Critical routes for Lighthouse CI
|
||||
|
||||
Lighthouse CI runs against a curated list of routes that represent the user journey:
|
||||
|
||||
- `/auth/login` — sign-in landing, first-impression critical;
|
||||
- `/` — post-auth home/dashboard;
|
||||
- `/accessibility` and `/accessibilite` — must themselves be perf-and-a11y exemplary (they are the portal's public proof);
|
||||
- a flagship route per integrated feature, added as features land (one route per feature lib).
|
||||
|
||||
Each route is scored against the thresholds above. **Failure on any threshold is blocking** — the merge is rejected.
|
||||
|
||||
### Where Lighthouse CI runs
|
||||
|
||||
| Environment | Cadence | Purpose | Blocking? |
|
||||
| --- | --- | --- | --- |
|
||||
| **CI on every PR** | per push | Catch regressions before merge | **Yes** |
|
||||
| **CI scheduled (weekly)** on prod env | cron in `security-scheduled.yml` from [ADR-0015](0015-cicd-gitea-actions.md), extended to cover perf | Detect drift / regressions in real environment | Reports as alerts; doesn't block, but triggers triage |
|
||||
| **Local dev** | manual via `pnpm nx run portal-shell:lighthouse` | Developer-side feedback | Non-blocking |
|
||||
|
||||
### Variability mitigation
|
||||
|
||||
Lighthouse scores fluctuate ±2–5 points run-to-run on the same code due to runner variance. The CI configuration:
|
||||
- runs **3 iterations** per route and uses the **median** (configurable via `lighthouserc.js`);
|
||||
- uses a fixed runtime profile (CPU throttling, network throttling matching "Slow 4G" baseline) so scores are comparable across runs and across machines;
|
||||
- pins the Lighthouse version per release of the runner image (avoids score shifts from Lighthouse updates).
|
||||
|
||||
### Bundle analysis — diagnosis tooling
|
||||
|
||||
`source-map-explorer` is wired as an Nx target (`pnpm nx run portal-shell:analyze`) to investigate budget breaches when they happen. Not part of CI — diagnostic tool only. The CI signal is the budget failure; `analyze` tells the developer **where** the weight comes from.
|
||||
|
||||
### Back-end perf budgets
|
||||
|
||||
Per-endpoint-family p95 / p99 SLOs, observed via the OpenTelemetry span data already produced by [ADR-0012](0012-observability-pino-opentelemetry.md). Initial budgets:
|
||||
|
||||
| Endpoint family | p95 | p99 |
|
||||
| --- | --- | --- |
|
||||
| `GET /auth/me` | 50 ms | 150 ms |
|
||||
| `GET /auth/login` (redirect) | 80 ms | 200 ms |
|
||||
| `GET /auth/callback` (token exchange) | 600 ms | 1500 ms — bound by Entra |
|
||||
| Read endpoints (DB-backed, simple) | 80 ms | 250 ms |
|
||||
| Read endpoints (DB-backed, complex) | 300 ms | 800 ms |
|
||||
| Write endpoints | 200 ms | 600 ms |
|
||||
| Downstream-API-orchestrating endpoints | bound by downstream + 50 ms BFF overhead | bound by downstream + 200 ms BFF overhead |
|
||||
|
||||
These budgets are **not** enforced as hard CI gates — load profile in CI is unrepresentative. They are enforced as:
|
||||
- **alerting thresholds** in the production observability backend (chosen in the future infrastructure ADR);
|
||||
- **review-time signals** on PRs that touch hot paths (the OTel data from staging informs the reviewer);
|
||||
- **quarterly perf review** — tightened or loosened with ADR amendment.
|
||||
|
||||
### Performance regressions are bugs, not backlog
|
||||
|
||||
A regression on any of the front-end gates blocks the merge. A regression on a back-end SLO that surfaces in production (alert fires) is triaged with the same priority as a security finding — root-caused, fixed, post-mortemed if it took the SLO out for more than 24 h.
|
||||
|
||||
### a11y / perf trade-off
|
||||
|
||||
Performance optimisation must not hurt accessibility. Some specific patterns to avoid:
|
||||
- skipping FOUC prevention (briefly unstyled content disorients users with cognitive disabilities);
|
||||
- removing focus-visible polyfills "for bundle size";
|
||||
- aggressive lazy-loading that delays critical interactive elements;
|
||||
- compression of imagery to the point of losing visual clarity required by low-vision users.
|
||||
|
||||
When the perf budget and the a11y bar conflict, **a11y wins** — the perf budget is then re-evaluated at the next quarterly review with the data point.
|
||||
|
||||
### CI integration
|
||||
|
||||
`package.json` script:
|
||||
|
||||
```jsonc
|
||||
"scripts": {
|
||||
"ci:perf": "pnpm exec nx build portal-shell --configuration=production && pnpm exec lhci autorun --config=./lighthouserc.js"
|
||||
}
|
||||
```
|
||||
|
||||
`lighthouserc.js` (illustrative, lands with the scaffold) declares:
|
||||
- the URLs to test (the critical-routes list);
|
||||
- 3 iterations, median report;
|
||||
- assertions matching the thresholds in this ADR;
|
||||
- upload target = local filesystem in CI (HTML reports kept as build artefacts).
|
||||
|
||||
The `perf` gate from [ADR-0015](0015-cicd-gitea-actions.md) is now wired and blocking.
|
||||
|
||||
### RUM strategy
|
||||
|
||||
No browser-side RUM SDK in v1. The OTel browser tracing from [ADR-0012](0012-observability-pino-opentelemetry.md) provides distributed-trace data for full user actions; combined with the Lighthouse CI scheduled prod runs and the BFF OTel spans, this gives sufficient signal to detect regressions. A v2 ADR will revisit if a real RUM tool is needed (concrete trigger: incidents that staging + Lighthouse + OTel didn't catch).
|
||||
|
||||
### Consequences
|
||||
|
||||
* Good, because performance discipline is structural — every PR is measured against the same bar, on every route that matters.
|
||||
* Good, because Core Web Vitals + Lighthouse CI is the industry-standard combo; results are comparable to public benchmarks and easy to explain to non-developers.
|
||||
* Good, because Angular `budgets` blocks bundle bloat at build time, before it ever reaches a user.
|
||||
* Good, because the BFF SLOs reuse the OTel pipeline already shipped by [ADR-0012](0012-observability-pino-opentelemetry.md) — no parallel telemetry.
|
||||
* Good, because the explicit a11y/perf trade-off rule prevents "performance regressions" from being introduced under the guise of optimisation.
|
||||
* Bad, because Lighthouse score variability (±2–5 points) is real; CI must mitigate it with median-of-3 runs and pinned tooling. Otherwise gates flake.
|
||||
* Bad, because tightening budgets at quarterly review requires discipline — quarterly meetings to actually happen, data to be reviewed, ADRs to be amended. Mitigated by making it a recurring calendar item.
|
||||
* Bad, because the back-end SLOs are advisory in CI (not enforced) — the only enforcement is in production alerting, which means a hot-path regression can ship and only fire later. Acknowledged: enforcing perf in CI requires a representative load profile we don't have.
|
||||
|
||||
### Confirmation
|
||||
|
||||
* `lighthouserc.js` exists at the repo root with the critical-routes list, the assertions matching the thresholds above, and a 3-iteration median configuration.
|
||||
* `apps/portal-shell/project.json` declares `budgets` of type `"error"` with the values above.
|
||||
* `package.json` exposes `ci:perf`, runnable locally with the same exit code as CI.
|
||||
* CI's `perf` gate from [ADR-0015](0015-cicd-gitea-actions.md) calls `pnpm ci:perf` and is blocking.
|
||||
* `apps/portal-shell` exposes a Nx target `analyze` invoking `source-map-explorer` against the production build's source maps.
|
||||
* The BFF p95/p99 budgets are documented in `apps/portal-bff/README.md` and translate into alert rules in the production observability backend (configured per future infrastructure ADR).
|
||||
* The `security-scheduled.yml` workflow includes a weekly Lighthouse CI run against the prod URL set, with reports uploaded as build artefacts and an alert on any threshold breach.
|
||||
* Quarterly performance review is on the team calendar; the agenda includes (a) review of the past quarter's reports, (b) decision to tighten budgets if achievable, (c) ADR amendment if budgets are adjusted.
|
||||
|
||||
## Pros and Cons of the Options
|
||||
|
||||
### Metrics scope
|
||||
|
||||
#### Google Core Web Vitals + supplementary + bundle size (chosen)
|
||||
|
||||
* Good, because aligned with the industry, comparable to public benchmarks, well-documented.
|
||||
* Good, because INP captures real user interaction latency (replaces FID since March 2024).
|
||||
* Good, because bundle size is the leading indicator that catches issues before they manifest as bad CWV.
|
||||
|
||||
#### Custom metrics only
|
||||
|
||||
* Bad, because un-benchmarkable, easy to be wrong about. Premature.
|
||||
|
||||
#### Lighthouse score only
|
||||
|
||||
* Bad, because the score is a weighted aggregate; gameable, hides which metric is failing. Use it as a top-line confirmation, not as the only metric.
|
||||
|
||||
### Front-end tooling
|
||||
|
||||
#### Lighthouse CI (chosen)
|
||||
|
||||
* Good, because mature, free, the standard for automated CWV in CI.
|
||||
* Good, because integrates trivially with any CI runner; uploads HTML reports as build artefacts for triage.
|
||||
* Bad, because score variability needs mitigation (median of 3, pinned version) — covered.
|
||||
|
||||
#### WebPageTest API
|
||||
|
||||
* Good, because more granular network/profile control.
|
||||
* Bad, because requires a paid account or self-hosted instance; less plug-and-play in CI.
|
||||
|
||||
#### Calibre / Speedlify
|
||||
|
||||
* Good, because modern, well-presented dashboards.
|
||||
* Bad, because younger / less established than Lighthouse CI; reconsidered later if Lighthouse CI proves insufficient.
|
||||
|
||||
### Threshold values
|
||||
|
||||
#### Google "Good" + Lighthouse ≥ 90 (chosen)
|
||||
|
||||
* Good, because the de facto industry baseline; exceeding it is a clear positive signal.
|
||||
* Bad, because not the strictest possible — a brand whose mission is accessibility might warrant tighter values. Mitigated by quarterly tightening.
|
||||
|
||||
#### Stricter (LCP ≤ 2 s, score ≥ 95)
|
||||
|
||||
* Good, because reflects an a11y-first mission.
|
||||
* Bad, because flaky in CI runners — false negatives cost more than the marginal user benefit, especially given runner variance.
|
||||
|
||||
### RUM
|
||||
|
||||
#### None in v1 (chosen)
|
||||
|
||||
* Good, because zero v1 ops surface; no privacy disclosure to make about browser-side data collection.
|
||||
* Bad, because real-user perf data is genuinely useful. Acceptable to defer because Lighthouse CI scheduled prod runs cover the gap.
|
||||
|
||||
#### RUM SDK (Sentry, Datadog Browser)
|
||||
|
||||
* Good, because real data, real users, real distribution.
|
||||
* Bad, because adds a runtime SDK to the SPA, sends data to a third party (or our own collector with cost), and requires a privacy-disclosure update to the accessibility/legal pages.
|
||||
|
||||
## More Information
|
||||
|
||||
* Web Vitals overview: https://web.dev/vitals/
|
||||
* Core Web Vitals (Google): https://web.dev/articles/vitals
|
||||
* Lighthouse CI: https://github.com/GoogleChrome/lighthouse-ci
|
||||
* Angular budgets: https://angular.dev/tools/cli/build#configuring-size-budgets
|
||||
* `source-map-explorer`: https://github.com/danvk/source-map-explorer
|
||||
* Related ADRs: [ADR-0004](0004-frontend-stack-angular-csr-zoneless-signals.md) (Angular stack — CSR perf profile), [ADR-0012](0012-observability-pino-opentelemetry.md) (OTel — basis for BFF SLOs), [ADR-0015](0015-cicd-gitea-actions.md) (CI gates — `perf` slot now wired), [ADR-0016](0016-accessibility-baseline-wcag-aa-targeted-aaa.md) (a11y / perf trade-off rule).
|
||||
@@ -60,3 +60,4 @@ ADRs are listed in numerical order. To slice by topic, filter on the `Tags` colu
|
||||
| [0014](0014-downstream-api-access-obo-pattern.md) | Downstream API access — On-Behalf-Of pattern, unified `DownstreamApiClient`, audience-aware authorization | accepted | `security`, `backend` | 2026-04-29 |
|
||||
| [0015](0015-cicd-gitea-actions.md) | CI/CD pipeline — Gitea Actions, trunk-based + squash-merge, thin YAML over portable scripts | accepted | `infrastructure`, `process` | 2026-04-30 |
|
||||
| [0016](0016-accessibility-baseline-wcag-aa-targeted-aaa.md) | Accessibility baseline — WCAG 2.2 AA + targeted AAA, Angular CDK + spartan-ng + Tailwind, APF panel testing | accepted | `accessibility`, `frontend`, `process` | 2026-04-30 |
|
||||
| [0017](0017-performance-budgets-lighthouse-ci.md) | Performance budgets — Core Web Vitals + Lighthouse CI gates, bundle budgets, BFF p95/p99 SLOs | accepted | `performance`, `frontend`, `backend`, `process` | 2026-04-30 |
|
||||
|
||||
Reference in New Issue
Block a user