Skip to content

perf(web): take workspace provisioning off the navigation critical path (AP-256)#397

Merged
isuttell merged 1 commit into
mainfrom
ap-256-web-navigation-latency
Jun 6, 2026
Merged

perf(web): take workspace provisioning off the navigation critical path (AP-256)#397
isuttell merged 1 commit into
mainfrom
ap-256-web-navigation-latency

Conversation

@isuttell

@isuttell isuttell commented Jun 6, 2026

Copy link
Copy Markdown
Contributor

Summary

Authed navigation felt slow: clicking a sidebar item paused for ~1s+ before anything painted. The _authed layout loader was blocking on a synchronous POST /v1/auth/web/callback round-trip (a DB write) before rendering. This splits identity resolution (free, from the validated token) from workspace provisioning (the DB write), and moves the write off the navigation critical path.

It also adds query-cache staleTime so repeat navigation serves cache instead of refetching, and defers non-critical loads.

Changes

  • Split the authed-layout loader (web-loaders.ts):
    • loadAuthedSession now resolves identity from the validated WorkOS token alone — no API call — so the layout paints immediately.
    • provisionWebMemberSession keeps the /v1/auth/web/callback DB write intact, but the layout fires it after first paint via a cached webSessionQuery (at most one provisioning round-trip per stale window, not one per navigation).
  • Per-query staleTime (queries.ts): lists 2m, stable resources (keys/settings/billing/session) 5m. Window-focus and explicit invalidation (mutations, SSE) still force fresh reads.
  • Access-links route migrated from a bare loader fn to the query cache (ensureQueryData + useSuspenseQuery, invalidation-based refresh).
  • Artifact detail: revisions only feed the secondary "create access link" panel, so they load via useQuery in the background instead of gating first paint.
  • Billing return: parallelized the status + invoices fetch.
  • Upstream-timing logs in the web API client (api-client.ts): method/path/status/request_id only — never token or body. Lets us measure the API-side latency work that's next on AP-256.

Risk

Low, web-only. No schema/migration changes. The callback write still happens on every authed session (just not blocking paint, and deduped per stale window). First-run default-key secret is correctly read from the cached session (it's only returned once on first provisioning). The dashboard's 100-artifact fetch was intentionally not trimmed — its live/expiring/total stats are computed client-side from that full list and the workspace API exposes no counts; trimming would silently break the stats. That belongs to a separate API contract change.

Where the real latency is (next phase, same ticket)

Axiom traces of authenticated preview traffic show the dominant cost is server-side in agent-paste-api-preview, not the web layer: ~2.7s per /v1/web/* call, driven by (1) a per-request WorkOS JWKS fetch (~660ms p95) and (2) a ~1.2s Hyperdrive/Neon connection stall. This web change is the correct, independent first step; the API-side caching + connection work is the high-leverage follow-up tracked on AP-256.

Test plan

  • pnpm --filter @agent-paste/web lint — clean
  • pnpm --filter @agent-paste/web typecheck — clean
  • pnpm --filter @agent-paste/web test:coverage — 332 passed, coverage above floors (stmt 93.3 / branch 86.55 / fn 95.37 / line 94.86)
  • Deployed working tree to preview (agent-paste-web-preview); / 307, /dashboard 200, no 5xx
  • Manual: navigate the sidebar logged in and confirm pages paint without the pre-paint pause

🤖 Generated with Claude Code

@linear-code

linear-code Bot commented Jun 6, 2026

Copy link
Copy Markdown
AP-256 Cut authed route loader latency (~1s blank wait on sidebar navigation)

Problem

Clicking a sidebar page in apps/web blocks for ~1s before the new page renders, because each authed route's TanStack loader awaits one or more API calls before the route is allowed to paint. AP-255 adds a loading indicator so the wait feels responsive; this ticket reduces the actual latency so the wait is short or gone.

All file:line references below are from a read-only pass over apps/web/src.

Candidate levers (ranked by expected impact)

1. Every authed navigation does a blocking POST to /v1/auth/web/callback — investigate first

apps/web/src/routes/_authed.tsx:14-19 runs loadAuthedSessionFn() on every child route. That loader (apps/web/src/server/web-loaders.ts:50-64) does await apiFetchOrEmpty("/v1/auth/web/callback", { method: "POST", ... }) on every entry — so dashboard → artifacts → keys each re-POSTs. This is the layout loader that gates all authed pages, so its latency is paid on every single navigation. Action: determine whether this callback is idempotent/read-only. If yes, move it to ensureQueryData(authedSessionQuery()) with a multi-minute staleTime so it's skipped on revisit. If it's genuinely side-effectful (session refresh), document why and explore making it non-blocking. This is likely the largest single win — verify with a network trace before assuming.

2. Dashboard over-fetches 100 artifacts to render 6

apps/web/src/server/web-loaders.ts:66-78 fetches /v1/web/artifacts?limit=${COUNT_LIMIT} with COUNT_LIMIT = 100 (:27), but the dashboard renders only RECENT_LIMIT = 6 (apps/web/src/routes/_authed.dashboard.tsx:14, .slice(0, RECENT_LIMIT)). The /artifacts list page fetches its own full list separately, so lowering the dashboard limit to ~6–10 doesn't break pagination. Action: lower the dashboard loader's artifact limit to match what's shown above the fold.

3. No per-query staleTime → repeat navigations refetch

Global default is staleTime: 10_000 (apps/web/src/router.tsx:12) and defaultPreloadStaleTime: 0 (:27). Query factories in apps/web/src/lib/queries.ts:34-93 (dashboardQuery, artifactsQuery, auditQuery, keysQuery, settingsQuery, billingQuery, …) set no staleTime, so revisiting a page inside the window still triggers a background refetch and a cold revisit blocks. Action: add per-query staleTime overrides for stable data (audit/keys/settings/billing on the order of minutes; artifacts shorter). Tune to acceptable staleness; window-focus refetch still catches updates.

4. access-links route bypasses the query cache entirely

apps/web/src/routes/_authed.access-links.tsx:11-12 calls listAccessLinksFn() directly in the loader (not ensureQueryData) and reads via Route.useLoaderData(), so every visit refetches regardless of staleTime. Action: migrate to ensureQueryData(accessLinksQuery()) + useSuspenseQuery for cache + invalidation parity with the other pages.

5. activateBillingReturn awaits serially

apps/web/src/server/web-loaders.ts:164-176: status is awaited before invoices starts. The normal loadBilling() (:150) already uses Promise.all. Action: Promise.all the two fetches here too — saves ~500ms–1s on the Checkout return redirect.

6. Defer the artifact-detail revisions query

apps/web/src/routes/_authed.artifacts.$artifactId.tsx:23-32 blocks the route on three parallel queries including artifactRevisionsQuery, but revisions are a secondary dropdown interaction, not above-the-fold. Action: defer() the revisions query and wrap that section in <Suspense>/<Await> so the viewer paints sooner.

7. (Profile-gated) per-request auth memoization

getServerAuth() (apps/web/src/server/authkit.ts:19-35) is re-invoked by each loader in a request. Likely <10ms each but compounds across 3 loaders. Action: only if a profile shows >~50ms total, memoize within request scope. Don't add the seam speculatively.

Approach

Start with a network trace of a single sidebar navigation (DevTools or the worker logs) to confirm where the ~1s actually goes before changing code — levers are ranked by expected impact, not measured. Lever #1 (the per-nav auth callback POST) is the prime suspect; confirm it. Land the safe, obviously-correct cuts (#2, #5) regardless. Treat #3 as a product call on acceptable data staleness.

Done

  • A captured before/after network trace (or worker timing log) of one sidebar navigation showing the loader critical-path time dropped meaningfully (target: cold authed navigation well under the current ~1s; name the measured number).
  • /v1/auth/web/callback per-navigation cost is either eliminated on revisit (cached) or documented as necessarily blocking with the reason.
  • Dashboard loader fetches only what it renders.
  • No regression in data freshness that matters (mutations still reflect immediately via invalidation; verify create-key / publish flows still show new rows).
  • pnpm test + web component tests green.

Relationship

Companion to AP-255 (the loading-indicator affordance). This ticket is the underlying speed; AP-255 is the perceived responsiveness. They can land independently.

Review in Linear

@coderabbitai

coderabbitai Bot commented Jun 6, 2026

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 24bdeacb-a254-4803-a65c-62902fd8f6f2

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch ap-256-web-navigation-latency

Comment @coderabbitai help to get the list of available commands and usage tips.

…th (AP-256)

Authed navigation blocked on a synchronous `/v1/auth/web/callback` round-trip
in the `_authed` layout loader before any page could paint. Split the loader
into two parts:

- `loadAuthedSession` now resolves identity from the validated WorkOS token
  alone (no API call), so the layout paints immediately.
- `provisionWebMemberSession` keeps the callback DB write intact but the layout
  fires it after first paint via a cached `webSessionQuery`, so it runs at most
  once per stale window instead of once per navigation.

Also:
- Add per-query `staleTime` so repeat navigation serves cache instead of
  refetching (lists 2m, stable resources 5m); window-focus and explicit
  invalidation still force fresh reads.
- Migrate the access-links route to the query cache (was a bare loader fn).
- Defer artifact-detail revisions: they only feed the secondary access-link
  panel, so they load via `useQuery` instead of gating first paint.
- Parallelize the billing-return status+invoices fetch.
- Add cheap upstream-timing logs in the web API client (method/path/status/
  request_id only, never token or body) so the API-side latency work tracked
  on AP-256 can be measured.

Note: Axiom traces show the dominant cost (~2.7s) is server-side in the API
worker (per-request WorkOS JWKS fetch + a Hyperdrive/Neon connection stall),
not the web layer. That work is the next phase on AP-256. This change is the
correct web-side improvement and ships independently.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@isuttell isuttell force-pushed the ap-256-web-navigation-latency branch from 0bd98a0 to 48974da Compare June 6, 2026 14:58
@isuttell isuttell enabled auto-merge (squash) June 6, 2026 14:58
@isuttell isuttell merged commit 73aea7b into main Jun 6, 2026
10 checks passed
@isuttell isuttell deleted the ap-256-web-navigation-latency branch June 6, 2026 14:59
@github-actions

github-actions Bot commented Jun 6, 2026

Copy link
Copy Markdown

agent-paste PR preview resources were cleaned up. The shared Preview GitHub Environment is retained for future preview deploys.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant