Audience: Contributors (frontend developers) and Operators
This document defines the performance budgets for every page route in RemitWise, explains how they are measured and enforced, and provides guidance for adding budgets when new routes are introduced.
For interactive frame budgets (scroll, click, animation handlers) see frame-budget-rules.md. For the request gateway, rate limiting, and structured logging see infrastructure.md.
- Why per-route budgets?
- Route tiers and budgets
- Budget definitions
- Current route map with budgets
- Measurement
- Enforcement
- Adding a budget for a new route
- Exceeding a budget
- CI integration
Not every page has the same complexity or user expectation. A landing page must be fast because it is the first impression. A dashboard aggregates data from multiple contracts and is allowed more time. An admin page is rarely visited and can tolerate a heavier payload. Per-route budgets let us:
- Catch regressions early (before they ship).
- Prioritise optimisation work where users feel it most.
- Give contributors clear, verifiable targets instead of vague "make it fast" guidance.
Every route is assigned to one of four tiers. The tier determines the Lighthouse performance score floor and the largest contentful paint (LCP) ceiling used during CI audits.
| Tier | Lighthouse score floor | LCP ceiling | Description |
|---|---|---|---|
| 1 — Critical | ≥ 90 | ≤ 2.5 s | Landing, dashboard, send money. First impression or core transaction. |
| 2 — Feature | ≥ 80 | ≤ 3.0 s | Authenticated feature pages (bills, split, insurance, family, goals, insights, transactions). |
| 3 — Utility | ≥ 70 | ≤ 4.0 s | Settings, search, tutorials, receipts. Used less frequently or on-demand. |
| 4 — Admin | ≥ 60 | ≤ 5.0 s | Debug, admin, API docs. Internal-only, not user-facing. |
- Tier 1 mirrors the existing Lighthouse test (
tests/e2e/dashboard-performance.spec.ts) which asserts a score ≥ 90 for/dashboard. The 2.5 s LCP aligns with Google's "good" threshold for Core Web Vitals. - Tier 2 allows slightly more time because these pages load authenticated data from contracts, cache layers, and multiple API calls. A score of 80 still feels fast to users.
- Tier 3 pages are accessed on-demand; users navigate to them with intent and accept a brief wait.
- Tier 4 pages are internal tooling. Performance is still tracked to prevent bloat, but the bar is lower.
Each route has four measurable budget components:
| Component | What it measures | How it is checked |
|---|---|---|
| Lighthouse performance score | Overall performance grade (0–100) | Lighthouse audit in Playwright E2E test |
| LCP (Largest Contentful Paint) | Time until the largest visible element renders | Lighthouse audit (reported in lhr.audits) |
| Route handler duration | Server-side time to generate the response | Middleware durationMs logged per request; available at GET /api/metrics |
| Bundle size | JavaScript payload delivered to the browser | Build-time check (see CI integration) |
These are the server-side budgets for the middleware durationMs metric. They complement the Lighthouse numbers by capturing backend latency independently of network conditions.
| Tier | Route handler duration ceiling |
|---|---|
| 1 — Critical | ≤ 200 ms |
| 2 — Feature | ≤ 500 ms |
| 3 — Utility | ≤ 1 000 ms |
| 4 — Admin | ≤ 2 000 ms |
Note: These budgets apply to the route handler execution time (the
durationMslogged bymiddleware.ts), not total page load time. They ensure the server responds quickly; client-side rendering accounts for the rest of the user-visible load.
| Route | Tier | Lighthouse floor | LCP ceiling | Handler ceiling |
|---|---|---|---|---|
/ |
1 | 90 | 2.5 s | 200 ms |
/dashboard |
1 | 90 | 2.5 s | 200 ms |
/send |
1 | 90 | 2.5 s | 200 ms |
/split |
2 | 80 | 3.0 s | 500 ms |
/bills |
2 | 80 | 3.0 s | 500 ms |
/insurance |
2 | 80 | 3.0 s | 500 ms |
/family |
2 | 80 | 3.0 s | 500 ms |
/dashboard/goals |
2 | 80 | 3.0 s | 500 ms |
/dashboard/insight |
2 | 80 | 3.0 s | 500 ms |
/dashboard/transaction-history |
2 | 80 | 3.0 s | 500 ms |
/transactions |
2 | 80 | 3.0 s | 500 ms |
/financial-insights |
2 | 80 | 3.0 s | 500 ms |
/emergency-transfer |
2 | 80 | 3.0 s | 500 ms |
/settings |
3 | 70 | 4.0 s | 1 000 ms |
/search |
3 | 70 | 4.0 s | 1 000 ms |
/tutorial |
3 | 70 | 4.0 s | 1 000 ms |
/tutorial/[tutorialId] |
3 | 70 | 4.0 s | 1 000 ms |
/tutorial/[tutorialId]/chapter/[chapterId] |
3 | 70 | 4.0 s | 1 000 ms |
/receipt/[txHash] |
3 | 70 | 4.0 s | 1 000 ms |
/debug |
4 | 60 | 5.0 s | 2 000 ms |
/admin |
4 | 60 | 5.0 s | 2 000 ms |
/api/docs |
4 | 60 | 5.0 s | 2 000 ms |
Lighthouse runs headless Chrome against each route and produces a performance score (0–100) plus individual audit results including LCP, FID/INP, CLS, FCP, and TBT.
The existing E2E test (tests/e2e/dashboard-performance.spec.ts) demonstrates the pattern:
import { assertDashboardPerformanceBudget } from '@/lib/performance/lighthouse-budget';
const result = await lighthouse(url, {
chromePath: chromium.executablePath(),
chromeFlags: ['--headless', '--no-sandbox', '--disable-gpu'],
output: 'json',
onlyCategories: ['performance'],
extraHeaders: {
'x-playwright-test': 'true',
Cookie: cookieValue,
},
});
const score = result.lhr.categories.performance.score ?? 0;
expect(assertDashboardPerformanceBudget(score)).toBeGreaterThanOrEqual(90);The helper assertDashboardPerformanceBudget in lib/performance/lighthouse-budget.ts normalises the 0–1 score to 0–100 and throws if it falls below the threshold. For other tiers, use normalizeLighthouseScore directly with the appropriate threshold:
import { normalizeLighthouseScore, PERFORMANCE_BUDGET_THRESHOLD } from '@/lib/performance/lighthouse-budget';
const normalized = normalizeLighthouseScore(score);
if (normalized < 80) {
throw new Error(`Feature page Lighthouse score ${normalized} is below 80.`);
}Every API request is timed by middleware.ts and logged as structured JSON with a durationMs field. See metrics-logging.md for the log format.
In-memory counters are also exposed at GET /api/metrics (admin only). Operators can poll this endpoint or feed the structured logs into an aggregator (Datadog, Grafana, CloudWatch) to track p50/p95/p99 latencies per route.
Sentry traces are captured at 10% in production (client) and 5% (edge). When tracing is sampled, Sentry records transaction durations and reports them in the Performance dashboard. Use the Sentry transaction name (typically the route path) to filter by route.
Note: Sentry sampling means 90% of production traces are dropped. Do not rely on Sentry alone for budget enforcement; use the Lighthouse E2E tests as the primary gate.
Enforcement happens at three layers:
The Lighthouse E2E test runs on every PR that touches page components or layout files. It asserts the Lighthouse performance score against the tier's floor. This is the primary enforcement mechanism for client-side load-time budgets.
To run it locally:
npm run test:perfThis executes tests/e2e/dashboard-performance.spec.ts. To add similar tests for other routes, copy the pattern and adjust the URL and threshold.
The npm run build step produces a production bundle. While there is no automated size-limit gate yet, contributors should review the build output for unexpected bundle size increases. If a route's JavaScript chunk grows by more than 10% after a change, investigate and optimise before merging. Use @next/bundle-analyzer to analyze and inspect chunk details (see the Bundle Analysis Guide for usage instructions).
Future enhancement: integrate size-limit or automate budget checks in CI.
Operators should monitor the structured logs for durationMs values exceeding the route handler ceiling. A simple alert rule:
# Example Datadog/Grafana alert
- alert: SlowRouteHandler
expr: durationMs > 500
for: 5m
labels:
severity: warning
annotations:
summary: "Route {{ $labels.path }} handler taking > 500ms (p95)"When you create a new page route:
-
Assign a tier based on the route's user impact (see Route tiers and budgets). Use your judgement; when in doubt, pick the more conservative (lower-numbered) tier.
-
Add the route to the table in Current route map with budgets and to this document.
-
Write a Lighthouse E2E test if the route is Tier 1 or Tier 2. Copy the pattern from
tests/e2e/dashboard-performance.spec.ts:- Authenticate (nonce → sign → login).
- Run Lighthouse against the route URL.
- Assert the score against the tier's floor using
normalizeLighthouseScoreor a dedicated helper.
-
Add a performance budget helper (optional) in
lib/performance/if the route has specific constraints beyond the generic tier budgets. -
Verify that the route's
loading.tsxskeleton provides stable layout blocks (see component-states.md).
If a Lighthouse audit or runtime metric exceeds its budget:
- Identify the bottleneck. Use Chrome DevTools Performance tab to record a load session. Look for long tasks (> 50 ms) on the main thread.
- Check common culprits:
- Large unoptimised images (use
next/image). - Unnecessary client-side JavaScript (use
next/dynamicwithssr: falsefor heavy components). - Waterfalling API calls (parallelise with
Promise.allor use the dashboard aggregate pattern). - Missing
loading.tsx(users see a blank screen during server render).
- Large unoptimised images (use
- Fix and re-audit. Run
npm run test:perf(or the equivalent test for your route) to confirm the budget is met. - Document any intentional overrides. If a route genuinely needs to exceed its tier budget (e.g. a data-heavy insight page), add a comment in the E2E test and update this document with the new threshold and rationale.
| Check | Command | Enforced in CI |
|---|---|---|
| Lighthouse score (dashboard) | npm run test:perf |
Yes — blocks merge on failure |
| Unit tests | npm run test:unit |
Yes |
| Integration tests | npm run test:integration |
Yes |
| E2E tests | npm run test:e2e |
Yes |
| Build | npm run build |
Yes |
| Lint | npm run lint |
Yes |
To enforce budgets for additional routes, add new test files under tests/e2e/ following the dashboard performance test pattern. Each test should:
- Authenticate using the nonce → sign → login flow.
- Run
lighthouse()against the route URL. - Assert the score with the appropriate tier threshold.
Register the new test in the test:perf script in package.json:
"test:perf": "npx playwright test tests/e2e/dashboard-performance.spec.ts tests/e2e/your-route-performance.spec.ts"- Automated
size-limitintegration for per-chunk JavaScript budgets. - Lighthouse CI server for tracking score trends over time.
- Sentry performance alerting tied to the tier handler ceilings.
| Document | Relevance |
|---|---|
| BUNDLE_ANALYSIS.md | How to run and interpret bundle size analyses |
| frame-budget-rules.md | Interactive handler budgets (scroll ≤ 16 ms, click ≤ 100 ms) |
| metrics-logging.md | Structured JSON log format and durationMs field |
| infrastructure.md | Request gateway, rate limiting, and middleware flow |
| architecture.md | Full route map and library layers |
| component-states.md | Loading skeleton patterns for stable layout |
| CONTRIBUTING.md | Test commands and PR expectations |
Last Updated: 2026-07-24