Skip to content

perf(web): load page code on first use, not at boot - #1946

Open
Makisuo wants to merge 5 commits into
mainfrom
perf/lazy-page-modules
Open

Makisuo wants to merge 5 commits into
mainfrom
perf/lazy-page-modules

Conversation

@Makisuo

@Makisuo Makisuo commented Oct 29, 2026 •

Copy link
Copy Markdown
Collaborator

Problem

In prd every cold dashboard load spends about 0.7 s (p50) to 2.4 s (p90) after the HTML before the app's init runs (maple.pageload.init_ms: p50 805 ms, p75 1339 ms, p90 2472 ms, 179 loads, 2026-10-27 to 2026-10-29). Every data read waits for init.

Cause

  • apps/web/src/page/registry.ts:18-92 (on main) statically imports all 75 page modules. Each index.ts imports the page's update, view, commands and subscriptions. So the startup graph holds every page: 307 chunks, 2724 KB gzip, 10.0 MB raw.
  • All of it is evaluated in one long task before init. Locally that is 231 ms at 1x CPU and 1247 ms at 4x.
  • A per-module profile at 4x CPU attributes about 150 ms to page module bodies. The rest is the runtime work that scales with page count (about 150 page subscription fibers, the root Model codec).

Fix

  • Each page's index.ts now holds only its entry: definePage(id, { Model, Message }, { routes, key }) and entry.lazy(pageCode.<Id>) (the shared import in page/code.ts).
  • The page code moves to implementation.ts (implementPage(entry, spec)). This is option (c) of docs/foldkit-decisions/s2-bundle.md. 74 pages were split by a codemod and /mcp by hand.
  • The registry loads a page's code with loadPageCode (page/registry.ts). update and view read the loaded code; a page slot exists only once its code is loaded.
  • Root (update.ts loadPage): when the route's page code is missing, the open page is left and the code loads once (Model.pageCode: Ready | Loading | Failed). The skeleton shows meanwhile; the page enters when the code arrives.
  • boot.ts (index.html's first script) starts the route's page code right after Clerk, beside the app bundle. page/code.ts holds every page's import and a path table that mirrors route/routers.ts. Each page's index.ts registers its loader there, so the registry's loadPageCode reuses the import boot started and never fetches twice.
  • The import is static on purpose. Chrome holds a dynamic import() of a module the app's own graph also imports until that whole graph has loaded. With a dynamic import the page chunk was requested at about 1.04 s at 4x CPU on the preview, after init, the same as with no early start. Static, it is requested at about 0.4 s.
  • init (main.ts) still asks for the route's page code beside LoadClerk / CheckSession; that picks up the started import.
  • Link intent on an unloaded page loads its code first, then runs that page's preload. This is the order the old router used for code-split routes.
  • A failed load shows "Couldn't load this page" with Reload. A stale chunk after a deploy reads as "Maple was updated".
  • Subscriptions: only the open page's Subscriptions ever ran. They now run in 24 fixed registry slots (page.0 ... page.23), slot i for the page's i-th Subscription. Restart and keep-alive rules are the same per Subscription. A page with more Subscriptions than slots fails to load, so it cannot drop one silently (max today: 15).
  • LoadPageCode is kept as a span with maple.page.id and maple.page.code_for_preload.
  • Dev with HMR loads every page before start, because a restored Model skips init.
  • Behavior is unchanged otherwise: same routes, same requests, same auth, same parking and revive.

Before / After

Bundle (local, production vite build, static graph from index.html and boot, measured before the rebase):

before after
startup chunks 307 294
startup JS gzip 2724 KB 1747 KB (-36%)
startup JS raw 10.0 MB 6.0 MB
all JS gzip 4251 KB 4457 KB (+5%, 75 page chunks)

Local, signed-in Home. Production builds served from disk with gzip. Self-hosted session stubbed, other API calls 503. Headless Chrome, 9 runs per row (7 for warm 4x), interleaved, medians. "init" is the app's first render; "page" is when the Home page body replaces the skeleton. Local numbers, no network latency.

init before init after page before page after long task before after
cold, 1x CPU 397 ms 318 ms 445 ms 389 ms 231 ms 171 ms
cold, 4x CPU 1525 ms 1121 ms 1647 ms 1417 ms 1247 ms 889 ms
warm, 1x CPU 312 ms 268 ms 319 ms 312 ms 136 ms 116 ms
warm, 4x CPU 1002 ms 787 ms 1118 ms 1055 ms 695 ms 558 ms
  • These local runs predate the early start from boot.ts. Locally the page gained less than init because the page chunk's fetch was on the critical path after init.
  • With the early start, boot's static graph also holds page/code.ts (about 30 KB gzip with the Clerk boot code). It downloads in parallel with the Schema chunk (58 KB gzip) boot already needed.

Preview (not prd). Cold, signed-in loads on app-pr-1946 (head 8e82ad5) against a fresh baseline preview of main at 0827fd4 (#1963, an empty commit, closed after the runs). Headless Chrome, a new browser context per load (no cache, saved session), CPU throttled with CDP, real network. Interleaved, 8 loads per row, medians. 0 failed loads of 96. "init" is maple.pageload.init_ms on the pageload <Page> spans in Maple (deployment.environment = pr-<n>), matched to each load by time, all 96 matched. "first render" is when main#main-content first exists. "body shows" is when it stops being the loading skeleton.

CPU page init first render body shows body after init
1x Home baseline 486 ms 628 ms 628 ms 140 ms
1x Home this PR 440 ms 568 ms 568 ms 117 ms
1x Traces baseline 484 ms 614 ms 614 ms 130 ms
1x Traces this PR 414 ms 546 ms 546 ms 131 ms
1x Services baseline 480 ms 605 ms 605 ms 131 ms
1x Services this PR 424 ms 566 ms 566 ms 135 ms
4x Home baseline 1303 ms 1833 ms 1833 ms 534 ms
4x Home this PR 1058 ms 1471 ms 1471 ms 444 ms
4x Traces baseline 1306 ms 1809 ms 1809 ms 484 ms
4x Traces this PR 1059 ms 1345 ms 1572 ms 500 ms
4x Services baseline 1336 ms 1839 ms 1839 ms 516 ms
4x Services this PR 1070 ms 1447 ms 1447 ms 373 ms
  • The page body shows earlier than the baseline on every row: 39 to 68 ms at 1x, 237 to 392 ms at 4x.
  • Body after init is now about the same as the baseline (it was 1006 vs 244 ms in the last run, before the early start).
  • The network was fast in this session (HTML done at about 40 ms). The last run had a slow, unstable network; these runs do not cover that case.
  • An earlier round with the dynamic-import version of the start had the body 27 to 130 ms later than the baseline at 1x. That is the version the static import fixed.

How to verify after deploy

Boot timing per release, prd (expect init_p50 to drop with the release):

SELECT ResourceAttributes['service.version'] ver, count() n,
  round(quantile(0.5)(toFloat64OrZero(SpanAttributes['maple.pageload.init_ms']))) init_p50,
  round(quantile(0.9)(toFloat64OrZero(SpanAttributes['maple.pageload.init_ms']))) init_p90
FROM traces WHERE $__orgFilter AND $__timeFilter(Timestamp) AND ServiceName = 'maple-web' AND SpanName LIKE 'pageload %'
  AND ResourceAttributes['deployment.environment'] = 'production' AND SpanAttributes['maple.pageload.init_ms'] != ''
GROUP BY ver ORDER BY ver

The page code load on the boot path (when it ends relative to the pageload):

SELECT count() n,
  round(quantile(0.5)(dateDiff('millisecond', p.Timestamp, c.Timestamp) + c.Duration / 1e6)) loaded_p50,
  round(quantile(0.9)(dateDiff('millisecond', p.Timestamp, c.Timestamp) + c.Duration / 1e6)) loaded_p90
FROM traces p INNER JOIN traces c ON c.TraceId = p.TraceId AND c.ParentSpanId = p.SpanId
WHERE $__orgFilter(p) AND $__orgFilter(c) AND p.Timestamp >= now() - INTERVAL 1 DAY AND c.Timestamp >= now() - INTERVAL 1 DAY
  AND p.ServiceName = 'maple-web' AND p.SpanName LIKE 'pageload %' AND c.SpanName = 'LoadPageCode'
  AND p.ResourceAttributes['deployment.environment'] = 'production'

Tests

  • apps/web/src/page/code.test.ts, against the real route parser and registry:
    • Each page route's URL maps to the page the registry renders it with.
    • Every page has a boot path; paths without a page start nothing.
    • Every registry page loads through the shared import in page/code.ts, and loadPageCode reuses the import boot started.
  • apps/web/src/page-code.test.ts, with a fresh registry:
    • init asks for the route's page code beside the session check.
    • An unloaded route shows the skeleton, loads once and then enters.
    • Leaving for an unloaded page.
    • Link intent loads the code, then runs the page's preload.
    • A failed load shows the error and Reload reloads the route.
    • Subscription slots restart with a new instance and stay idle for a page with fewer Subscriptions.
  • Root and registry tests load every page first (loadEveryPage). That also enforces the slot count.

Follow-ups

  • Measure a slow network (for example a 3G profile) on a preview; the runs above had a fast one.
  • The boot path table mirrors route/routers.ts. code.test.ts fails if a page route maps elsewhere or a page has no path; a second route added to an existing page without a path only loses the early start.
  • Foldkit builds a JSON codec of the whole root Model at start, for dev model preservation, even in production. Making it lazy saved about 75 ms at 4x CPU locally (about 6%). That fix belongs upstream in foldkit.
  • @maple/domain/http schema construction is still about 180 ms of eval at 4x CPU, because the API clients need the whole contract at start.
  • Some page Model/Message files import another page's index and pull its update and view back into startup. Examples: logs/list/message.ts → logs/detail, traces/detail/model.ts → span-panel, service-map/message.ts → service-map/map, and onboarding quick-start → integrations/embed. Pointing them at the child's model/message modules would shrink startup further.
  • Dev (import.meta.hot) loads every page before start, because a restored Model skips init. So the Loading and Failed paths only run in builds and tests.

🤖 Generated with Claude Code


View with [code]smith Autofix with [code]smith
Need help on this PR? Tag @codesmith-bot with what you need.

@coderabbitai

coderabbitai Bot commented Oct 29, 2026 •

Copy link
Copy Markdown

Important

Review skipped

Too many files!

This PR contains 175 files, which is 25 over the limit of 150.

To get a review, reduce the PR to 150 files or fewer by splitting it into smaller PRs or changing its base branch.

Upgrade to a paid plan to raise the limit.

This review couldn't start because sufficient usage credits or metered capacity aren't available. Add credits or update usage-based reviews in the billing tab, then retry.

Run configuration
  • Source mode: Auto
  • Repository: Unavailable
  • Branch: Unavailable
  • File: No configuration file from this repository applied
  • Commit: Unavailable
  • Layers: defaults
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: fa74ba7b-206c-46d6-b060-f5c8dd325c93
Commits

Reviewing files that changed from the base of the PR and between 0827fd4 and 8e82ad5.

Files selected for processing: 175
  • apps/web/src/boot.ts
  • apps/web/src/command.ts
  • apps/web/src/entry.ts
  • apps/web/src/main.ts
  • apps/web/src/message.ts
  • apps/web/src/model.ts
  • apps/web/src/page-code.test.ts
  • apps/web/src/page/agent-sessions/list/implementation.ts
  • apps/web/src/page/agent-sessions/list/index.ts
  • apps/web/src/page/agent-sessions/session/implementation.ts
  • apps/web/src/page/agent-sessions/session/index.ts
  • apps/web/src/page/agent-sessions/tool/implementation.ts
  • apps/web/src/page/agent-sessions/tool/index.ts
  • apps/web/src/page/agent-sessions/tools/implementation.ts
  • apps/web/src/page/agent-sessions/tools/index.ts
  • apps/web/src/page/alerts/editor/implementation.ts
  • apps/web/src/page/alerts/editor/index.ts
  • apps/web/src/page/alerts/hub/implementation.ts
  • apps/web/src/page/alerts/hub/index.ts
  • apps/web/src/page/alerts/incident/implementation.ts
  • apps/web/src/page/alerts/incident/index.ts
  • apps/web/src/page/alerts/rule/implementation.ts
  • apps/web/src/page/alerts/rule/index.ts
  • apps/web/src/page/alerts/uptime-form/implementation.ts
  • apps/web/src/page/alerts/uptime-form/index.ts
  • apps/web/src/page/alerts/uptime-monitor/implementation.ts
  • apps/web/src/page/alerts/uptime-monitor/index.ts
  • apps/web/src/page/analytics/implementation.ts
  • apps/web/src/page/analytics/index.ts
  • apps/web/src/page/anomalies/detail/implementation.ts
  • apps/web/src/page/anomalies/detail/index.ts
  • apps/web/src/page/anomalies/list/implementation.ts
  • apps/web/src/page/anomalies/list/index.ts
  • apps/web/src/page/chrome.ts
  • apps/web/src/page/code-review/implementation.ts
  • apps/web/src/page/code-review/index.ts
  • apps/web/src/page/code.test.ts
  • apps/web/src/page/code.ts
  • apps/web/src/page/contract.ts
  • apps/web/src/page/dashboards/list/implementation.ts
  • apps/web/src/page/dashboards/list/index.ts
  • apps/web/src/page/dashboards/templates/implementation.ts
  • apps/web/src/page/dashboards/templates/index.ts
  • apps/web/src/page/dashboards/view/implementation.ts
  • apps/web/src/page/dashboards/view/index.ts
  • apps/web/src/page/dashboards/widget/implementation.ts
  • apps/web/src/page/dashboards/widget/index.ts
  • apps/web/src/page/dashboards/widget/widget-leave.scene.test.ts
  • apps/web/src/page/errors/hub/implementation.ts
  • apps/web/src/page/errors/hub/index.ts
  • apps/web/src/page/errors/issue/implementation.ts
  • apps/web/src/page/errors/issue/index.ts
  • apps/web/src/page/infra-integrations/aws/list/implementation.ts
  • apps/web/src/page/infra-integrations/aws/list/index.ts
  • apps/web/src/page/infra-integrations/aws/resource/implementation.ts
  • apps/web/src/page/infra-integrations/aws/resource/index.ts
  • apps/web/src/page/infra-integrations/cloudflare/list/implementation.ts
  • apps/web/src/page/infra-integrations/cloudflare/list/index.ts
  • apps/web/src/page/infra-integrations/cloudflare/worker/implementation.ts
  • apps/web/src/page/infra-integrations/cloudflare/worker/index.ts
  • apps/web/src/page/infra-integrations/cloudflare/zone/implementation.ts
  • apps/web/src/page/infra-integrations/cloudflare/zone/index.ts
  • apps/web/src/page/infra-integrations/gcp/list/implementation.ts
  • apps/web/src/page/infra-integrations/gcp/list/index.ts
  • apps/web/src/page/infra-integrations/gcp/workload/implementation.ts
  • apps/web/src/page/infra-integrations/gcp/workload/index.ts
  • apps/web/src/page/infra-integrations/planetscale/db/implementation.ts
  • apps/web/src/page/infra-integrations/planetscale/db/index.ts
  • apps/web/src/page/infra-integrations/planetscale/list/implementation.ts
  • apps/web/src/page/infra-integrations/planetscale/list/index.ts
  • apps/web/src/page/infra-integrations/railway/list/implementation.ts
  • apps/web/src/page/infra-integrations/railway/list/index.ts
  • apps/web/src/page/infra-integrations/railway/service/implementation.ts
  • apps/web/src/page/infra-integrations/railway/service/index.ts
  • apps/web/src/page/infra-integrations/supabase/list/implementation.ts
  • apps/web/src/page/infra-integrations/supabase/list/index.ts
  • apps/web/src/page/infra-integrations/supabase/project/implementation.ts
  • apps/web/src/page/infra-integrations/supabase/project/index.ts
  • apps/web/src/page/infra/containers/detail/implementation.ts
  • apps/web/src/page/infra/containers/detail/index.ts
  • apps/web/src/page/infra/containers/list/implementation.ts
  • apps/web/src/page/infra/containers/list/index.ts
  • apps/web/src/page/infra/hosts/detail/implementation.ts
  • apps/web/src/page/infra/hosts/detail/index.ts
  • apps/web/src/page/infra/hosts/list/implementation.ts
  • apps/web/src/page/infra/hosts/list/index.ts
  • apps/web/src/page/infra/kubernetes/node/implementation.ts
  • apps/web/src/page/infra/kubernetes/node/index.ts
  • apps/web/src/page/infra/kubernetes/nodes/implementation.ts
  • apps/web/src/page/infra/kubernetes/nodes/index.ts
  • apps/web/src/page/infra/kubernetes/pod/implementation.ts
  • apps/web/src/page/infra/kubernetes/pod/index.ts
  • apps/web/src/page/infra/kubernetes/pods/implementation.ts
  • apps/web/src/page/infra/kubernetes/pods/index.ts
  • apps/web/src/page/infra/kubernetes/service/implementation.ts
  • apps/web/src/page/infra/kubernetes/service/index.ts
  • apps/web/src/page/infra/kubernetes/workload/implementation.ts
  • apps/web/src/page/infra/kubernetes/workload/index.ts
  • apps/web/src/page/infra/kubernetes/workloads/implementation.ts
  • apps/web/src/page/infra/kubernetes/workloads/index.ts
  • apps/web/src/page/infra/overview/implementation.ts
  • apps/web/src/page/infra/overview/index.ts
  • apps/web/src/page/instance.scene.test.ts
  • apps/web/src/page/integrations/implementation.ts
  • apps/web/src/page/integrations/index.ts
  • apps/web/src/page/investigations/detail/implementation.ts
  • apps/web/src/page/investigations/detail/index.ts
  • apps/web/src/page/investigations/list/implementation.ts
  • apps/web/src/page/investigations/list/index.ts
  • apps/web/src/page/logs/list/implementation.ts
  • apps/web/src/page/logs/list/index.ts
  • apps/web/src/page/logs/log/implementation.ts
  • apps/web/src/page/logs/log/index.ts
  • apps/web/src/page/metrics/browse/implementation.ts
  • apps/web/src/page/metrics/browse/index.ts
  • apps/web/src/page/metrics/detail/implementation.ts
  • apps/web/src/page/metrics/detail/index.ts
  • apps/web/src/page/overview/implementation.ts
  • apps/web/src/page/overview/index.ts
  • apps/web/src/page/preload.test.ts
  • apps/web/src/page/recommendations/implementation.ts
  • apps/web/src/page/recommendations/index.ts
  • apps/web/src/page/registry.ts
  • apps/web/src/page/releases/detail/implementation.ts
  • apps/web/src/page/releases/detail/index.ts
  • apps/web/src/page/releases/list/implementation.ts
  • apps/web/src/page/releases/list/index.ts
  • apps/web/src/page/replays/detail/implementation.ts
  • apps/web/src/page/replays/detail/index.ts
  • apps/web/src/page/replays/list/implementation.ts
  • apps/web/src/page/replays/list/index.ts
  • apps/web/src/page/revive.test.ts
  • apps/web/src/page/security/finding/implementation.ts
  • apps/web/src/page/security/finding/index.ts
  • apps/web/src/page/security/run/implementation.ts
  • apps/web/src/page/security/run/index.ts
  • apps/web/src/page/security/run/run.scene.test.ts
  • apps/web/src/page/security/tabs/implementation.ts
  • apps/web/src/page/security/tabs/index.ts
  • apps/web/src/page/service-map/implementation.ts
  • apps/web/src/page/service-map/index.ts
  • apps/web/src/page/services/detail/implementation.ts
  • apps/web/src/page/services/detail/index.ts
  • apps/web/src/page/services/list/implementation.ts
  • apps/web/src/page/services/list/index.ts
  • apps/web/src/page/settings/account/implementation.ts
  • apps/web/src/page/settings/account/index.ts
  • apps/web/src/page/settings/implementation.ts
  • apps/web/src/page/settings/index.ts
  • apps/web/src/page/settings/ingest-rule/implementation.ts
  • apps/web/src/page/settings/ingest-rule/index.ts
  • apps/web/src/page/settings/mcp-page/implementation.ts
  • apps/web/src/page/settings/mcp-page/index.ts
  • apps/web/src/page/share/implementation.ts
  • apps/web/src/page/share/index.ts
  • apps/web/src/page/sources/detail/implementation.ts
  • apps/web/src/page/sources/detail/index.ts
  • apps/web/src/page/sources/list/implementation.ts
  • apps/web/src/page/sources/list/index.ts
  • apps/web/src/page/sources/new/implementation.ts
  • apps/web/src/page/sources/new/index.ts
  • apps/web/src/page/traces/detail/implementation.ts
  • apps/web/src/page/traces/detail/index.ts
  • apps/web/src/page/traces/list/implementation.ts
  • apps/web/src/page/traces/list/index.ts
  • apps/web/src/platform/telemetry.ts
  • apps/web/src/subscription.ts
  • apps/web/src/update.customer-grace.test.ts
  • apps/web/src/update.pending-task.test.ts
  • apps/web/src/update.test.ts
  • apps/web/src/update.ts
  • apps/web/src/view.lazy.test.ts
  • apps/web/src/view.scene.test.ts
  • apps/web/src/view.ts
  • docs/foldkit-decisions/s8-pages.md

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

  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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

@maple-review-bot

maple-review-bot Bot commented Oct 29, 2026 •

Copy link
Copy Markdown

Maple review

🟡 Confidence 6/10 · needs attention
Held at 6 because 22 reviewed files were not read.
2 warnings −2 · risk medium · 22 files not read

Splits every page into a small index.ts entry plus a lazily imported implementation.ts, and adds Model.pageCode so a route enters only once its code has arrived, behind a skeleton or a reloadable failure. The split is faithful to main; one preload path loses its cancellation check.

  • Each page's index.ts holds only definePage plus entry.lazy(() => import("./implementation"))
  • loadPageCode caches page code; loadPage awaits it and shows a skeleton or PageCode.Failed
  • Page subscriptions run in 24 fixed registry slots instead of one per page
  • LoadPageCode stays a kept command span carrying maple.page.id

Findings

🟠 Warning · F1 · LoadPageCode completion preloads a hover the user already cancelled

correctness · apps/web/src/update.ts:487

This branch runs preloadLink whenever the page code arrives, without checking that the link intent still exists. Hovering a nav link to an unloaded page starts LoadPageCode; if the pointer leaves before the chunk arrives (EndedLinkIntent → Preload.ended), the completion still runs that page's preload Commands, so every abandoned hover fires the page's reads and holds their answers. Preload.elapsed removes the intent before the load starts, so the intent has to stay registered until the code arrives for this branch to check it the way ElapsedPreloadDelay does (update.ts:1130).

Keep the intent (do not `Preload.ended` it when the page's code still has to load) and gate this step on `Preload.elapsed(model.preload, preloadHref, version)` returning `Some`, as the `ElapsedPreloadDelay` handler does.
🟠 Warning · F2 · A hover that ends before the page code arrives still runs that page's preloads

correctness · apps/web/src/update.ts:487

loadedPageCode fires preloadLink whenever the page code arrives, without the liveness check that ElapsedPreloadDelay makes through Preload.elapsed. Preload.elapsed has already cleared the intent before LoadPageCode was issued, so a hover that ends while the chunk is in flight (EndedLinkIntent → ended) is indistinguishable from a live one and the page's preload Commands run anyway, firing reads for a page the user left.

Keep the intent registered until the code arrives (don't clear it while the page's code is still unloaded) and gate the preload branch on `Preload.elapsed(model.preload, preloadHref, nowMs)` returning `Some`, as the `ElapsedPreloadDelay` handler does.
🤖 Prompt to fix all 2 findings with an AI agent
Findings from an automated review of commit 2b63ff8d33e279beacef55486f22ed194a51cd90. Verify each one against the current code before changing anything, fix only those that still apply, and keep each fix to the lines it names.

---

F1 · Warning · correctness · apps/web/src/update.ts:487
`LoadPageCode` completion preloads a hover the user already cancelled
This branch runs `preloadLink` whenever the page code arrives, without checking that the link intent still exists. Hovering a nav link to an unloaded page starts `LoadPageCode`; if the pointer leaves before the chunk arrives (`EndedLinkIntent` → `Preload.ended`), the completion still runs that page's preload Commands, so every abandoned hover fires the page's reads and holds their answers. `Preload.elapsed` removes the intent before the load starts, so the intent has to stay registered until the code arrives for this branch to check it the way `ElapsedPreloadDelay` does (`update.ts:1130`).
Suggested fix: Keep the intent (do not `Preload.ended` it when the page's code still has to load) and gate this step on `Preload.elapsed(model.preload, preloadHref, version)` returning `Some`, as the `ElapsedPreloadDelay` handler does.

---

F2 · Warning · correctness · apps/web/src/update.ts:487
A hover that ends before the page code arrives still runs that page's preloads
`loadedPageCode` fires `preloadLink` whenever the page code arrives, without the liveness check that `ElapsedPreloadDelay` makes through `Preload.elapsed`. `Preload.elapsed` has already cleared the intent before `LoadPageCode` was issued, so a hover that ends while the chunk is in flight (`EndedLinkIntent` → ended) is indistinguishable from a live one and the page's preload Commands run anyway, firing reads for a page the user left.
Suggested fix: Keep the intent registered until the code arrives (don't clear it while the page's code is still unloaded) and gate the preload branch on `Preload.elapsed(model.preload, preloadHref, nowMs)` returning `Some`, as the `ElapsedPreloadDelay` handler does.

Production impact

Open errors in the changed files
Issue Service Occurrences File
@maple/mcp/errors/McpQueryError maple-chat 429 apps/web/src/page/registry.ts
@maple/mcp/errors/McpQueryError maple-ai 226 apps/web/src/page/registry.ts
@maple/mcp/errors/McpQueryError maple-chat 222 apps/web/src/page/registry.ts
@maple/mcp/errors/McpQueryError maple-chat 75 apps/web/src/page/registry.ts
@maple/mcp/errors/McpQueryError maple-ai 63 apps/web/src/page/registry.ts
@maple/mcp/errors/McpQueryError maple-ai 36 apps/web/src/page/registry.ts
@maple/mcp/errors/McpQueryError maple-chat 30 apps/web/src/page/registry.ts
@maple/mcp/errors/McpQueryError maple-chat 24 apps/web/src/page/registry.ts
@maple/mcp/errors/McpQueryError maple-ai 24 apps/web/src/page/registry.ts
@maple/mcp/errors/McpQueryError maple-ai 20 apps/web/src/page/registry.ts

After this merges, Maple checks whether they stop.

Production traffic of the changed files (last 7 days)
File Calls/day Busiest operations
apps/web/src/page/settings/mcp-page/implementation.ts 15k POST /mcp 6.9k/day, 0.0% err
POST /mcp 6.9k/day, 0.0% err
GET /mcp 443/day, 0.0% err
apps/web/src/page/settings/mcp-page/index.ts 15k POST /mcp 6.9k/day, 0.0% err
POST /mcp 6.9k/day, 0.0% err
GET /mcp 443/day, 0.0% err
apps/web/src/platform/telemetry.ts 66 LoadClerk 66/day, 0.0% err
apps/web/src/page/traces/detail/implementation.ts 18 FetchSpanHierarchy 18/day, 0.0% err
apps/web/src/page/traces/detail/index.ts 18 FetchSpanHierarchy 18/day, 0.0% err
Telemetry this change adds and removes (2)
  • ➕ attribute maple.page.id · apps/web/src/command.ts:111
  • ➕ attribute maple.page.code_for_preload · apps/web/src/command.ts:112
What was checked
  • All 174 changed files read; page routes/key and spec content match main, with 8 widget/boot modules (chat/sheet, span-panel, timeline, waterfall, sign-in, onboarding) kept inline
  • LoadPageCode's tryPromise maps any chunk rejection to PageLoadError, so a stale chunk reaches PageCode.Failed with Reload instead of dying
  • Slot restart compares page, key, org, instance and the Subscription's own dependencies (registry.ts:451); parkPages keeps parked pages when entered is null
Files not reviewed (22)

The review ended before it read these diffs, so nothing above vouches for them.

  • apps/web/src/page/agent-sessions/session/index.ts
  • apps/web/src/page/agent-sessions/tool/implementation.ts
  • apps/web/src/page/agent-sessions/tool/index.ts
  • apps/web/src/page/alerts/editor/implementation.ts
  • apps/web/src/page/alerts/editor/index.ts
  • apps/web/src/page/alerts/hub/implementation.ts
  • apps/web/src/page/alerts/hub/index.ts
  • apps/web/src/page/alerts/incident/implementation.ts
  • apps/web/src/page/alerts/incident/index.ts
  • apps/web/src/page/chat/page/implementation.ts
  • apps/web/src/page/chat/page/index.ts
  • apps/web/src/page/code-review/implementation.ts
  • apps/web/src/page/code-review/index.ts
  • apps/web/src/page/dashboards/list/implementation.ts
  • apps/web/src/page/dashboards/list/index.ts
  • apps/web/src/page/security/run/implementation.ts
  • apps/web/src/page/security/run/index.ts
  • apps/web/src/page/security/run/run.scene.test.ts
  • apps/web/src/page/security/tabs/implementation.ts
  • apps/web/src/page/security/tabs/index.ts
  • apps/web/src/page/service-map/implementation.ts
  • apps/web/src/page/service-map/index.ts

2b63ff8 · Updated on every push. Reply "won't fix" to dismiss a finding, or mention @maple-review-bot to ask about one.

@Makisuo Makisuo added the preview:web Deploy a Workers-only PR preview (no ingest/Electric); see docs/pr-previews.md label Oct 29, 2026
@Makisuo
Makisuo deployed to pr-preview October 29, 2026 11:03 — with GitHub Actions Active

@maple-review-bot maple-review-bot Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

2 inline notes from Maple's review. The score and summary are in the review comment above.

Comment thread apps/web/src/update.ts
Comment thread apps/web/src/update.ts
@github-actions

github-actions Bot commented Oct 29, 2026 •

Copy link
Copy Markdown
Contributor

🍁 Maple PR preview

Note

Preview resources were removed when the preview label came off.

Final commit 8e82ad5 · View workflow run

@Makisuo
Makisuo deployed to pr-preview October 29, 2026 11:08 — with GitHub Actions Active
@Makisuo Makisuo removed the preview:web Deploy a Workers-only PR preview (no ingest/Electric); see docs/pr-previews.md label Oct 29, 2026
@Makisuo
Makisuo deployed to pr-preview October 29, 2026 19:49 — with GitHub Actions Active
The app evaluated every page module before init. Each page now keeps
only its schemas, routes and key in index.ts; the rest moves to
implementation.ts, which loads on first entry or link intent. The
route's page code loads at init beside the session check, and the
open page's Subscriptions run in fixed registry slots.
…undle

The page chunk only started downloading after init, so on a real network a
cold load showed the skeleton for its round trip. boot.ts now imports the
route's page code right after Clerk starts. Every page entry loads through
the same shared import in page/code.ts, so the registry's loadPageCode
picks up the started load instead of fetching again.
@Makisuo
Makisuo force-pushed the perf/lazy-page-modules branch from 2b63ff8 to 021c2bb Compare October 30, 2026 08:02
@Makisuo Makisuo added the preview:web Deploy a Workers-only PR preview (no ingest/Electric); see docs/pr-previews.md label Oct 30, 2026
@Makisuo
Makisuo deployed to pr-preview October 30, 2026 08:02 — with GitHub Actions Active
@maple-review-bot

maple-review-bot Bot commented Oct 30, 2026 •

Copy link
Copy Markdown

Maple review

🟡 Confidence 6/10 · needs attention
Held at 6 because 3 reviewed files were not read.
no findings · tests covered · risk medium · 2/2 new units observable · 3 files not read

Every page splits into a lazy implementation.ts loaded on first use, with Model.pageCode, 24 fixed subscription slots and a boot-time chunk prefetch; the splits I read are faithful. The open McpQueryError issues come from the MCP tool registry in apps/ai, not this web registry.

  • page/registry.ts loads a page's code on first use; update and view read the loaded implementation
  • loadPage leaves the open page until the route's code arrives, tracked in Model.pageCode
  • Page Subscriptions run in 24 fixed registry slots instead of one per page
  • boot.ts starts the route's page chunk beside the app bundle

Before merge

  • Manual · Wait for the in-progress Tests (@maple/web-1-of-2) and Tests (@maple/web-2-of-2) jobs to finish green on this head

Fixed since the last review

  • ✅ F1 · LoadPageCode completion preloads a hover the user already cancelled
  • ✅ F2 · A hover that ends before the page code arrives still runs that page's preloads

Production impact

Open errors in the changed files
Issue Service Occurrences File
@maple/mcp/errors/McpQueryError maple-chat 831 apps/web/src/page/registry.ts
@maple/mcp/errors/McpQueryError maple-ai 226 apps/web/src/page/registry.ts
@maple/mcp/errors/McpQueryError maple-chat 222 apps/web/src/page/registry.ts
@maple/mcp/errors/McpQueryError maple-chat 75 apps/web/src/page/registry.ts
@maple/mcp/errors/McpQueryError maple-ai 63 apps/web/src/page/registry.ts
@maple/mcp/errors/McpQueryError maple-ai 36 apps/web/src/page/registry.ts
@maple/mcp/errors/McpQueryError maple-chat 33 apps/web/src/page/registry.ts
@maple/mcp/errors/McpQueryError maple-chat 24 apps/web/src/page/registry.ts
@maple/mcp/errors/McpQueryError maple-ai 24 apps/web/src/page/registry.ts
@maple/mcp/errors/McpQueryError maple-chat 18 apps/web/src/page/registry.ts

After this merges, Maple checks whether they stop.

Production traffic of the changed files (last 7 days)
File Calls/day Busiest operations
apps/web/src/page/code.ts 23k POST /mcp 11k/day, 0.0% err
POST /mcp 11k/day, 0.0% err
GET /mcp 447/day, 0.0% err
apps/web/src/page/settings/mcp-page/implementation.ts 23k POST /mcp 11k/day, 0.0% err
POST /mcp 11k/day, 0.0% err
GET /mcp 447/day, 0.0% err
apps/web/src/page/settings/mcp-page/index.ts 23k POST /mcp 11k/day, 0.0% err
POST /mcp 11k/day, 0.0% err
GET /mcp 447/day, 0.0% err
apps/web/src/platform/telemetry.ts 100 LoadClerk 100/day, 0.0% err
apps/web/src/page/traces/detail/implementation.ts 22 FetchSpanHierarchy 22/day, 0.0% err
apps/web/src/page/traces/detail/index.ts 22 FetchSpanHierarchy 22/day, 0.0% err
What was checked
  • Every page's key, routes and Subscription count match the base file: git grep parity at both refs plus the page diffs I read (share, traces/detail, traces/list, sources/*)
  • Head preloads only for an intent still live at the delay (update.ts:463), as base does at update.ts:1105, so F1/F2 describe a timing shift rather than a new defect
  • pageCodeIdOf prefers literal segments, so /alerts/create and /dashboards/templates beat their : patterns; code.test.ts pins all 82 routes
Observability coverage: 2 of 2 changes observable
Change Kind Observable Evidence
Page code load (LoadPageCode command) background work yes command.ts:106-121 annotates maple.page.id and maple.page.code_for_preload; platform/telemetry.ts:40 keeps it in keepCommands
boot-time page chunk start (startRoutePageCode) boot step yes boot.ts:8 starts the same shared import (page/code.ts:4-16) that the following LoadPageCode span awaits
Files not reviewed (3)

The review ended before it read these diffs, so nothing above vouches for them.

  • apps/web/src/page/infra-integrations/railway/service/implementation.ts
  • apps/web/src/page/infra-integrations/railway/service/index.ts
  • apps/web/src/page/infra-integrations/supabase/list/index.ts

021c2bb · Updated on every push. Reply "won't fix" to dismiss a finding, or mention @maple-review-bot to ask about one.

@Makisuo
Makisuo deployed to pr-preview October 30, 2026 08:07 — with GitHub Actions Active
Chrome holds a dynamic import of a module that the app's own graph also
imports until that whole graph has loaded. On the preview the route's page
chunk was requested at about 1.04 s at 4x CPU, after init, the same as with
no early start. A static import puts page/code.ts in boot's own graph, so the
page chunk starts beside the app bundle.
@Makisuo
Makisuo deployed to pr-preview October 30, 2026 08:40 — with GitHub Actions Active
@maple-review-bot

maple-review-bot Bot commented Oct 30, 2026 •

Copy link
Copy Markdown

Maple review

🟢 Confidence 10/10 · safe to merge
The reviewed delta is a dynamic-to-static import swap in the web boot entry; the route table it drives has no ambiguous match and page/code.test.ts pins it.
no findings · tests covered · risk medium · 1/1 new units observable

Since the last review the only change is boot.ts importing page/code statically (plus a comment), so the route's chunk starts without waiting for the app graph. Safe to merge. The open @maple/mcp/errors/McpQueryError issues are in apps/ai's MCP tool registry, not the page registry their stacks name, and this PR does not address them.

  • boot.ts statically imports startRoutePageCode from page/code instead of a dynamic import()

Production impact

Open errors in the changed files
Issue Service Occurrences File
@maple/mcp/errors/McpQueryError maple-chat 831 apps/web/src/page/registry.ts
@maple/mcp/errors/McpQueryError maple-ai 226 apps/web/src/page/registry.ts
@maple/mcp/errors/McpQueryError maple-chat 222 apps/web/src/page/registry.ts
@maple/mcp/errors/McpQueryError maple-chat 75 apps/web/src/page/registry.ts
@maple/mcp/errors/McpQueryError maple-ai 63 apps/web/src/page/registry.ts
@maple/mcp/errors/McpQueryError maple-ai 36 apps/web/src/page/registry.ts
@maple/mcp/errors/McpQueryError maple-chat 33 apps/web/src/page/registry.ts
@maple/mcp/errors/McpQueryError maple-chat 24 apps/web/src/page/registry.ts
@maple/mcp/errors/McpQueryError maple-ai 24 apps/web/src/page/registry.ts
@maple/mcp/errors/McpQueryError maple-chat 18 apps/web/src/page/registry.ts

After this merges, Maple checks whether they stop.

Production traffic of the changed files (last 7 days)
File Calls/day Busiest operations
apps/web/src/page/code.ts 24k POST /mcp 12k/day, 0.0% err
POST /mcp 12k/day, 0.0% err
GET /mcp 457/day, 0.0% err
apps/web/src/page/settings/mcp-page/implementation.ts 24k POST /mcp 12k/day, 0.0% err
POST /mcp 12k/day, 0.0% err
GET /mcp 457/day, 0.0% err
apps/web/src/page/settings/mcp-page/index.ts 24k POST /mcp 12k/day, 0.0% err
POST /mcp 12k/day, 0.0% err
GET /mcp 457/day, 0.0% err
apps/web/src/platform/telemetry.ts 144 LoadClerk 126/day, 0.0% err
LoadPageCode 18/day, 0.0% err
apps/web/src/page/traces/detail/implementation.ts 22 FetchSpanHierarchy 22/day, 0.0% err
apps/web/src/page/traces/detail/index.ts 22 FetchSpanHierarchy 22/day, 0.0% err
apps/web/src/command.ts 18 LoadPageCode 18/day, 0.0% err
What was checked
  • pageCodeIdOf precedence: no two table entries tie on a matching path; /alerts/create, /dashboards/:/widgets/:, /infra/aws/:/* resolve as intended (ran the matcher over the table)
  • shared in page/code.ts keeps one import per page and drops a rejected promise so a reload can retry; boot.ts handles its rejection
  • page/code.ts still has no static imports, so the boot graph gains only the route table, not page code
Observability coverage: 1 of 1 changes observable
Change Kind Observable Evidence
boot.ts early start of the route's page chunk (startRoutePageCode) outbound chunk load yes The shared import it starts is measured by the LoadPageCode span in apps/web/src/command.ts:106-116 (maple.page.id, maple.page.code_for_preload)

8e82ad5 · Updated on every push. Reply "won't fix" to dismiss a finding, or mention @maple-review-bot to ask about one.

@Makisuo
Makisuo deployed to pr-preview October 30, 2026 08:42 — with GitHub Actions Active
@Makisuo Makisuo removed the preview:web Deploy a Workers-only PR preview (no ingest/Electric); see docs/pr-previews.md label Oct 30, 2026
@Makisuo
Makisuo deployed to pr-preview October 30, 2026 08:57 — with GitHub Actions Active

This branch was successfully deployed

1 active deployment
pr-preview — 8e82ad5e Deployed Oct 30, 2026 by Makisuo via deploy-pr-preview #7807
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