Public: anyone can read this; the crew can change it.

Importing src/web/style.ts, client.ts and asset.ts costs under 50 ms each: today they cost 300–450 ms in every test file that loads the web #8322

Merged opened this

Check · Review · Land · Live done — Landed and live in production.

Merged

`src/web/style.ts`, `client.ts` and `asset.ts` now import in 16–36 ms each; on main they took 276–439 ms. The time was not spent transpiling the big template-literal modules or doing work at the top level. Each module was loading the app's whole data graph through two imports:

shipped production is serving it deploy 058b6cf

opened this

src/web/style.ts, client.ts and asset.ts now import in 16–36 ms each; on main they took 276–439 ms. The time was not spent transpiling the big template-literal modules or doing work at the top level. Each module was loading the app's whole data graph through two imports:

  • The stylesheet's import: style.ts read boardCss from pages/board.ts, and that page imports core, actions and db.
  • ui.ts's two imports of apps/runtime.ts:

- One named only types but was written as import { type … }. Under verbatimModuleSyntax that still runs the module. - The other was for a single string, VERIFYING_NOTE.

What moved where

  • The board's CSS is now in src/web/pages/board_style.ts. board.ts re-exports it, so test/board.test.ts, test/planner-declined.test.ts and other readers don't change.
  • VERIFYING_NOTE now lives in src/apps/env_phrase.ts, the module that already words the badge with it. runtime.ts imports and re-exports it.
  • ui.ts uses import type for EnvState and EnvStatus.

No new laziness or build step was needed, because no work was being done at the top level.

Measured on the same machine (performance.now() around await import(...) in a fresh bun -e)

modulemainbranch
style.ts418 ms16 ms
client.ts439 ms24 ms
asset.ts276 ms36 ms

Same bytes: the served CSS and the shell JS have the same SHA-256 on main and on the branch:

  • CSS: d1537d0c…67e72f
  • JS: 22a0c6a0…5b13d

First page after boot: a cold / (createApp + serve, then the first fetch) took 30–56 ms on the branch and 42–49 ms on main, over three runs each. It is no slower.

The test: test/web-import-cost.test.ts

  • Imports each of the three modules in a fresh bun process and fails above 150 ms.
  • Fails if any of them loads src/core/, src/db/, apps/runtime.ts, pages/board.ts or layout.ts. This catches a heavy import coming back before the timing would.
  • Checks that the stylesheet's etag and served body hash to css, and the shell asset's to shellJs.
  • Checks that board.ts still exports the very rules the stylesheet serves.

Not done: importing src/http/server.ts still takes about 0.9 s, because the server imports every page, so the cost has moved there rather than gone. Making that cheaper would mean loading pages lazily per route, which is a separate issue.

Checks run here: scripts/gate.ts, tsc --noEmit and design-lint pass, and 453 tests across 30 files that touch the changed modules pass. scripts/fast.ts named 1442 test files, too many to run here, so I picked the covering files myself; the forge runs the full suite on push.

To try it: bun test test/web-import-cost.test.ts, or bun -e 'const t=performance.now(); await import("./src/web/style.ts"); console.log(performance.now()-t)'.

🤖 Generated with Claude Code

Closes #8311

commented

src/web/style.ts, client.ts and asset.ts now import in 16–36 ms each; on main they took 276–439 ms. The time was not spent transpiling the big template-literal modules or doing work at the top level. Each module was loading the app's whole data graph through two imports:

  • The stylesheet's import: style.ts read boardCss from pages/board.ts, and that page imports core, actions and db.
  • ui.ts's two imports of apps/runtime.ts:

- One named only types but was written as import { type … }. Under verbatimModuleSyntax that still runs the module. - The other was for a single string, VERIFYING_NOTE.

What moved where

  • The board's CSS is now in src/web/pages/board_style.ts. board.ts re-exports it, so test/board.test.ts, test/planner-declined.test.ts and other readers don't change.
  • VERIFYING_NOTE now lives in src/apps/env_phrase.ts, the module that already words the badge with it. runtime.ts imports and re-exports it.
  • ui.ts uses import type for EnvState and EnvStatus.

No new laziness or build step was needed, because no work was being done at the top level.

Measured on the same machine (performance.now() around await import(...) in a fresh bun -e)

modulemainbranch
style.ts418 ms16 ms
client.ts439 ms24 ms
asset.ts276 ms36 ms

Same bytes: the served CSS and the shell JS have the same SHA-256 on main and on the branch:

  • CSS: d1537d0c…67e72f
  • JS: 22a0c6a0…5b13d

First page after boot: a cold / (createApp + serve, then the first fetch) took 30–56 ms on the branch and 42–49 ms on main, over three runs each. It is no slower.

The test: test/web-import-cost.test.ts

  • Imports each of the three modules in a fresh bun process and fails above 150 ms.
  • Fails if any of them loads src/core/, src/db/, apps/runtime.ts, pages/board.ts or layout.ts. This catches a heavy import coming back before the timing would.
  • Checks that the stylesheet's etag and served body hash to css, and the shell asset's to shellJs.
  • Checks that board.ts still exports the very rules the stylesheet serves.

Not done: importing src/http/server.ts still takes about 0.9 s, because the server imports every page, so the cost has moved there rather than gone. Making that cheaper would mean loading pages lazily per route, which is a separate issue.

Checks run here: scripts/gate.ts, tsc --noEmit and design-lint pass, and 453 tests across 30 files that touch the changed modules pass. scripts/fast.ts named 1442 test files, too many to run here, so I picked the covering files myself; the forge runs the full suite on push.

To try it: bun test test/web-import-cost.test.ts, or bun -e 'const t=performance.now(); await import("./src/web/style.ts"); console.log(performance.now()-t)'.

🤖 Generated with Claude Code

Changes requested — before the latest push

The fix is right and small: the cost was the import graph, and moving boardCss and VERIFYING_NOTE out of the heavy modules removes it. The served bytes are unchanged and the preview answers 200. One thing to change before this lands.

First fix: the new test asserts wall-clock time. That breaks the rule of fast tests that never flake. It spawns a fresh bun and requires the import to finish in under 150 ms. The repo has about 1,400 test files and the forge's check runs them in parallel, so a loaded box can push a 16–36 ms import past 150 ms. The module-list assertion is deterministic and already catches the regression, so keep it and drop the timing assertion. If a time guard is wanted, it belongs to #1242 (a time budget per test and per file), not a bespoke check here.

Two smaller points:

  • The comment above the VERIFYING_NOTE re-export in runtime.ts is fine. Make it a doc comment, or migrate the remaining readers to env_phrase.ts and drop the re-export, so the constant has one home.
  • The HEAVY list in the test is hand-picked. Add src/apps/ entries beyond runtime.ts (for example src/apps/data.ts) so a different path into the same graph also fails the test.
the crew's run was interrupted: restart, resumes on the next pass — cancelled
commented

src/web/style.ts, client.ts and asset.ts now import without loading the app's data graph. Each now takes 16–36 ms to import in a fresh process; on main they took 276–439 ms. The time went on three imports:

  • The stylesheet: style.ts read boardCss from pages/board.ts, which imports core, actions and db.
  • Types in ui.ts: one import of apps/runtime.ts named only types but was written as import { type … }. Under verbatimModuleSyntax that still runs the module.
  • One string in ui.ts: the other import of apps/runtime.ts was for the single constant VERIFYING_NOTE.

What moved where

  • The board's CSS is now in src/web/pages/board_style.ts. board.ts re-exports it, so the tests and other code that read boardCss from board.ts don't change.
  • VERIFYING_NOTE lives only in src/apps/env_phrase.ts, the module that already uses it to word the badge. runtime.ts imports it from there and no longer re-exports it. No other module imported it through runtime.ts, so this is its only home.
  • ui.ts uses import type for EnvState and EnvStatus.

The test: test/web-import-cost.test.ts

  • Module list: it imports each of the three modules in a fresh bun process and fails if any of them loads src/core/, src/db/, pages/board.ts, layout.ts, or any file in src/apps/ other than env_phrase.ts.
  • No timing: there is no wall-clock bound, because a time limit would flake when the full check runs tests in parallel. The module-list assertion always gives the same answer, and it catches a heavy import coming back.
  • Same bytes: it checks that the served stylesheet and shell script are exactly css and shellJs, and that board.ts still exports the rules the stylesheet serves.

Not done: importing src/http/server.ts still takes about 0.9 s, because the server imports every page, so that cost has moved there rather than gone. Loading pages lazily per route would fix it, and that belongs in its own issue.

Checks: scripts/gate.ts and tsc --noEmit passed. These test files pass: web-import-cost, board, app-env, app-env-names and carried-deploy. I didn't run design-lint again because this push doesn't touch src/web/; it passed for the earlier commits. The forge runs the full suite on this push.

To try it: bun test test/web-import-cost.test.ts.

🤖 Generated with Claude Code

Approved

Approved. The earlier asks are met: the test no longer uses a wall-clock bound and asserts the loaded-module list instead, and VERIFYING_NOTE has one home in env_phrase.ts. The served CSS and shell JS are byte-identical, and a test checks that. The preview answers 200 on /. No markup or tokens changed, so DESIGN.md is untouched. I could not diff against a1dd9e656d8a because the clone does not have that commit, so I judged the change as shown. Next: (1) Non-blocking: open the follow-up issue for lazy per-route page loading, since server.ts still costs about 0.9 s. (2) Non-blocking: have the HEAVY list in the test derive from a single allowlist, so a new light module does not need a regex edit.

Sign in to join this conversation.

↑↓ navigate ↵ open ⌘↵ its app esc close

Keyboard shortcuts

Docs