* perf(bundle): drop the BAS chart and the Node crypto polyfill from the shared client baseline
Two chunks rode along in the first-load JS of almost every dashboard route:
the full BAS 2026 chart (315 KB uncompressed, in 81 route manifests) and
the browser polyfill for Node's crypto/vm/Buffer (327 KB, in 26 routes
incl. login and register). Neither was needed on first paint; both got
there through static imports of helpers that happen to live next to code
that needs the data or the builtin.
Node polyfill (4 pure splits, behaviour unchanged, re-exported from the
original modules for server callers):
- lib/auth/bankid-flags.ts: isBankIdEnabled (login, register, security
settings imported it from bankid.ts, which imports crypto).
- lib/import/bank-file/formats.ts: the format registry + detection (the
import history imported getFormat from parser.ts, which hashes).
- lib/salary/personnummer-format.ts: parsing/validation/formatting (the
employee forms reached the encrypting personnummer.ts via tax-column).
- lib/auth/api-key-scopes.ts: scope catalogue, groups, tool map, helpers
(the API key panel imported STAGING_SCOPES from the key generator).
BAS chart:
- lib/bookkeeping/bas-lazy.ts + use-bas-reference.ts: the chart becomes a
dynamic import, fetched once per session after first paint; components
that show BAS names/descriptions call useBasReference() and re-render
when it lands. Until then (and on the server) only the hardcoded
account-descriptions answer, so SSR and hydration agree.
- lib/bookkeeping/bas-labels.ts: class/group labels out of bas-reference.ts
(account-descriptions needed a label and paid for the whole chart).
- lib/bookkeeping/bas-account-numbers.ts (generated, ~11 KB) +
scripts/generate-bas-account-numbers.ts (--check) + parity test:
isStandardBASAccountNumber for AddAccountDialog/ChartOfAccountsManager.
- lib/bookkeeping/account-classifier-{heuristic,client}.ts: the BAS-aligned
heuristic shared by the server classifier and a client variant that uses
the lazy chart.
- lib/bookkeeping/invoice-accounts.ts: INVOICE_FX_RATE_MISSING,
InvoiceFxRateMissingError, getRevenueAccount, getOutputVatAccount out of
invoice-entries.ts, whose engine import pulled account-backfill and the
chart into SendInvoiceDialog/PaymentBookingDialog.
- CorrectOpeningBalanceDialog re-seeds names when the chart lands;
OpeningBalanceRowEditor builds its Fuse indexes lazily; the
ChartOfAccountsManager BAS-katalog tab awaits the chunk.
Tooling:
- scripts/perf/client-import-closure.mjs: static import closure of every
'use client' module with the shortest chain to a target (file or bare
specifier); found every path above without a build.
- scripts/checks/client-node-builtin.mjs wired into check:guards: a client
module reaching a Node builtin is a hard failure (0 today).
Left as is: invoices/[id], its credit page and SendInvoiceDialog still
reach the chart through lib/invoices/issue-credit-note -> invoice-entries
-> engine -> account-backfill; splitting the engine is out of scope here.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(perf): unambiguous import-edge regex in the closure walker (CodeQL js/redos)
One quantifier per span: a greedy [^'"]* up to the specifier quote, which it
cannot cross, so a run of whitespace has a single parse. Same edges as
before (multi-line named imports, re-exports, side-effect imports; type-only
imports still skipped).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
---------
Co-authored-by: Jakob Wennberg <311770904+jakobwennberg-oss@users.noreply.github.com>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Request latency measurement (auth proxy + route wrapper)
Why this exists: a customer reported that "it takes time before all fields load when clicking around" (2026-08-26). The API handlers themselves are fast (p50 38 ms); the cost is the number of sequential calls a page makes and the fixed per-request tax in front of each one (auth proxy + route wrapper). This page is the protocol for measuring that tax before and after every change in the responsiveness plan, so no PR claims a win without a number.
What is instrumented
| Surface | Where | Header | Log line |
|---|---|---|---|
Auth proxy (every page, RSC, prefetch, /api request) |
lib/supabase/middleware.ts via lib/supabase/proxy-timing.ts |
Server-Timing: mw-auth, mw-session, mw-company, mw-mfa, mw-total on page/RSC/prefetch responses; X-Proxy-Timing (same value) on /api responses |
proxy completed with kind (page, rsc, prefetch, api), route (ids and tokens collapsed), status, authMs, sessionMs, companyMs, mfaMs, totalMs |
Route wrapper (withRouteContext, 367 of 529 routes) |
lib/api/with-route-context.ts |
Server-Timing: auth, company, handler |
op completed with operation, status, durationMs, authMs, companyMs, handlerMs |
| Browser | @vercel/speed-insights mounted in app/layout.tsx |
n/a | Vercel dashboard > Speed Insights > Routes (p75 TTFB, FCP, LCP, INP, CLS per route) |
Phase meanings for the proxy: authMs = supabase.auth.getUser() (a network call to Supabase Auth); sessionMs = session-timeout cookie state (getClaims, HMAC verify, auto_logout read when re-minting); companyMs = resolve_active_company RPC including the write-back on fallback; mfaMs = assurance-level check plus listFactors() (a second network call) on the enforced-MFA path.
Reading the numbers
Percentiles per route from production logs (the vercel CLI is linked to the project; --limit defaults to 100, raise it):
# route wrapper, grouped by operation
vercel logs --environment production --since 24h --limit 1000 --json --query "op completed" \
| npx tsx scripts/perf/log-percentiles.ts --field durationMs,authMs,companyMs,handlerMs --group operation
# auth proxy, grouped by request kind and route
vercel logs --environment production --since 24h --limit 1000 --json --query "proxy completed" \
| npx tsx scripts/perf/log-percentiles.ts --field totalMs,authMs,sessionMs,companyMs,mfaMs --group kind,route
# auth proxy, one row per kind (the headline fixed cost)
vercel logs --environment production --since 24h --limit 1000 --json --query "proxy completed" \
| npx tsx scripts/perf/log-percentiles.ts --field totalMs,authMs,companyMs,mfaMs --group kind
If --limit above 100 is not honoured by the installed CLI, loop --since/--until windows and concatenate before piping. The Vercel MCP get_runtime_logs tool (group_by: route) gives request counts per route, not percentiles; use it for prefetch volume (kind=prefetch per page load).
In the browser: DevTools > Network, pick a document or _rsc request, Timing tab, the mw-* metrics show the proxy phases; /api responses show auth/company/handler from the route wrapper and the proxy numbers in the X-Proxy-Timing response header.
Request count per interaction (the number that maps to "fields load late"), in the console after each action on a production build (next build && next start):
performance.getEntriesByType('resource')
.filter((r) => /\/api\/|\/rest\/v1\//.test(r.name))
.map((r) => `${Math.round(r.startTime)} ${Math.round(r.duration)}ms ${r.name}`)
Interactions to record every time: /transactions soft navigation; open Bokför on a row; /bookkeeping then Nytt verifikat; /invoices then Ny faktura; /reports; /supplier-invoices/new; a list row to its detail page on customers and invoices. Note the request count and how many dependent rounds (start times that wait on an earlier response).
Targets (p75 unless stated)
| Metric | Target | Why |
|---|---|---|
Proxy page/rsc totalMs |
p50 < 40 ms, p90 < 100 ms | Two parallel DB waves at most, no auth network call |
Proxy api totalMs |
p50 < 5 ms | Local JWT verification only |
| Route wrapper auth + company | p50 < 45 ms (read), same for write | One company resolution per call, never two |
| TTFB (hard load) | < 400 ms | Layout on two DB waves |
| LCP | < 1.5 s | |
| INP | < 200 ms | The metric "clicking around" maps to |
| Reference-data requests per form open | 0 blocking | Seeded and cached client-side |
Protocol per PR
- Before merging: record the current numbers for the interactions above on a production build, and the last 24 h of
proxy completed/op completedpercentiles, in the PR description. - 24 h after the production deploy: re-run the same three commands and the same interactions; append one row per PR to the table below.
- A PR that claims a latency win without a before/after row is not done.
Baseline and results
| Date | Change | Proxy page p50/p90 | Proxy api p50/p90 | Route auth+company p50 | Notes |
|---|---|---|---|---|---|
| 2026-08-26 | Route wrapper only (proxy unmeasured) | n/a | n/a | 44 ms (auth 3, company 41) | 100-call sample; handler p50 38 ms, total p50 96 ms, p90 274 ms |