# Measure website performance

Start with `apps/www/scripts/profile-routes.ts --help`. It measures document navigation or client-side thread switching, keeps every sample, and reports medians and ranges. Runs are read-only: opening a thread never submits a message.

```sh
cd apps/www
bun run scripts/profile-routes.ts --require-ready --revision before --json \
  /houses /threads/<id> > /tmp/arbe-before.json
# Make one change, let hot reload finish, then repeat the identical command with
# --revision after and output /tmp/arbe-after.json.
bun run scripts/compare-performance.ts /tmp/arbe-before.json /tmp/arbe-after.json

# First visit and revisit from another thread in the same house:
bun run scripts/profile-routes.ts --require-ready --revision before --json \
  --switch-from /threads/<source> /threads/<target> > /tmp/arbe-switch-before.json
```

The local default is `https://arbe.localhost` with the `arbe-dev` auth profile. For a production baseline, pass `--origin https://arbe.0sk.ar --profile arbe-prod`; readiness measurements require a deployment containing the instrumentation. Setup and auth recovery are in the [browser-testing skill](../../../packages/skills/browser-testing/SKILL.md). The profiler always runs browser commands from the repository root, where `agent-browser.json` selects the session. Manual probes must use that same cwd. The browser command is `agent-browser auth login`; Arbe's separate CLI uses `arbe login`.

For fast production-build comparisons without deployment waits, the Cloudflare adapter supports `vite preview`. Build with `bun run --filter '@arbe/www' build`, then run `portless arbe-preview bunx vite preview` from `apps/www` on its own approved browser hostname. The package's `preview` script instead runs remote Wrangler. Pass `--build-dir apps/www/.svelte-kit/output` to the profiler from the repository root to detect changes to the served build rather than unrelated edits in the shared checkout. Restart only your preview process when switching saved builds; keep both variants' output immutable and compare on the same origin.

Use fixed threads the robot account can read; botlab is suitable. Include a short conversation and a longer one, and keep the same signal/tool display settings. Defaults are one discarded warmup and five measured samples. HTTP cache state is uncontrolled; these are document navigations in an existing browser session, not cold-start claims. Each switch sample starts with a new source document, visits the target, returns, and revisits; HTTP cache survives while app memory starts fresh. Keep browser, viewport, network conditions, analytics consent, and app origin unchanged. Fixture counts detect additions/removals, not edited message text; leave the fixture contents untouched. Local Vite results cannot stand in for production results.

Keep the shared browser and runtime source files idle during a run. Source fingerprints detect concurrent local code changes; they identify the checkout, not the deployed production revision. Supply the measured revision with `--revision`. Profiler fingerprints prevent comparisons across different measurement drivers. Redirects, missing readiness/paint, changed thread entry/message counts, and unfinished samples fail the run. Electric's HTTP 409 handle reset is accepted only when a later request to the same shape succeeds; unrecovered resets still fail. Preserve failed output as diagnostic evidence, never as a baseline. The comparison command rejects incompatible reports and recalculates deltas from raw samples; overlapping ranges warrant another run, not a claimed win. For a stronger causal check, restore only your optimization, repeat the baseline, then reapply and repeat.

Read the metrics as separate phases:

- `FCP` / `LCP` describe paint, sampled after the readiness wait.
- `shellReadyMs` marks the mounted header and route after DOM update and two animation frames; it does not mean every house collection has loaded.
- `threadReadyMs` additionally waits for successful history backfill, ready history/authors queries, and an authenticated composer, then DOM/scroll frames. Revisits can use previously loaded history; this does not promise a fresh server read. Rendered Markdown enhancements can finish later.
- Switch timings start immediately before the browser clicks a same-origin link and end at the target's fresh thread-ready mark. A full document reload fails the scenario.
- `settleMs` is the last completed loader/API response, a diagnostic only. Shape requests are excluded even when their initial data is needed; it is not readiness or a complete critical path.

The marks live in `apps/www/src/lib/performance.ts`, the root layout, and `Chat.svelte`. They stay in the browser's User Timing timeline. A navigation commit with `startObserved: false` means the start callback was missed; the profiler uses its own click timestamp, never substitutes the commit time. Save JSON reports with the change and report absolute timings, sample counts, ranges, origin, and the exact behavior changed. No measured improvement means say so and use the trace to choose the next candidate.

The [saved browser proof](../../../apps/www/proofs/performance/2026-09-07/README.md) includes reusable botlab fixtures, raw reports, rejected comparisons, and a repeat that exposed a misleading first-run gain.

The [local production-build experiments](../../../apps/www/proofs/performance/2026-09-07-local/README.md) record rejected optimizations and the cache conditions used; none established a repeatable latency gain. Keep raw traces and build snapshots in ignored local storage, and commit compact evidence instead.

Use `agent-browser vitals --json` for one current-page reading. Reach for `agent-browser trace start|stop` or `profiler start|stop` only after the route profiler identifies a route but not the expensive phase.

If local house/thread pages hang with `live=true` requests pending, confirm the browser is on `https://arbe.localhost` and run `portless doctor`. HTTP/1.1 on Vite's direct `:8888` port can exhaust its connection pool on Electric long-polls. See [local development](development.md).
