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
Check · Review · Land · Live done — Landed and live in production.
`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
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.tsreadboardCssfrompages/board.ts, and that page imports core, actions and db. ui.ts's two imports ofapps/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.tsre-exports it, sotest/board.test.ts,test/planner-declined.test.tsand other readers don't change. VERIFYING_NOTEnow lives insrc/apps/env_phrase.ts, the module that already words the badge with it.runtime.tsimports and re-exports it.ui.tsusesimport typeforEnvStateandEnvStatus.
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)
| module | main | branch |
|---|---|---|
| style.ts | 418 ms | 16 ms |
| client.ts | 439 ms | 24 ms |
| asset.ts | 276 ms | 36 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
bunprocess and fails above 150 ms. - Fails if any of them loads
src/core/,src/db/,apps/runtime.ts,pages/board.tsorlayout.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 toshellJs. - Checks that
board.tsstill 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
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_NOTEre-export inruntime.tsis fine. Make it a doc comment, or migrate the remaining readers toenv_phrase.tsand drop the re-export, so the constant has one home. - The HEAVY list in the test is hand-picked. Add
src/apps/entries beyondruntime.ts(for examplesrc/apps/data.ts) so a different path into the same graph also fails the test.
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.tsreadboardCssfrompages/board.ts, which imports core, actions and db. - Types in
ui.ts: one import ofapps/runtime.tsnamed only types but was written asimport { type … }. UnderverbatimModuleSyntaxthat still runs the module. - One string in
ui.ts: the other import ofapps/runtime.tswas for the single constantVERIFYING_NOTE.
What moved where
- The board's CSS is now in
src/web/pages/board_style.ts.board.tsre-exports it, so the tests and other code that readboardCssfromboard.tsdon't change. VERIFYING_NOTElives only insrc/apps/env_phrase.ts, the module that already uses it to word the badge.runtime.tsimports it from there and no longer re-exports it. No other module imported it throughruntime.ts, so this is its only home.ui.tsusesimport typeforEnvStateandEnvStatus.
The test: test/web-import-cost.test.ts
- Module list: it imports each of the three modules in a fresh
bunprocess and fails if any of them loadssrc/core/,src/db/,pages/board.ts,layout.ts, or any file insrc/apps/other thanenv_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
cssandshellJs, and thatboard.tsstill 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. 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.
src/web/style.ts,client.tsandasset.tsnow 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:style.tsreadboardCssfrompages/board.ts, and that page imports core, actions and db.ui.ts's two imports ofapps/runtime.ts:- One named only types but was written as
import { type … }. UnderverbatimModuleSyntaxthat still runs the module. - The other was for a single string,VERIFYING_NOTE.What moved where
src/web/pages/board_style.ts.board.tsre-exports it, sotest/board.test.ts,test/planner-declined.test.tsand other readers don't change.VERIFYING_NOTEnow lives insrc/apps/env_phrase.ts, the module that already words the badge with it.runtime.tsimports and re-exports it.ui.tsusesimport typeforEnvStateandEnvStatus.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()aroundawait import(...)in a freshbun -e)Same bytes: the served CSS and the shell JS have the same SHA-256 on main and on the branch:
d1537d0c…67e72f22a0c6a0…5b13dFirst 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.tsbunprocess and fails above 150 ms.src/core/,src/db/,apps/runtime.ts,pages/board.tsorlayout.ts. This catches a heavy import coming back before the timing would.css, and the shell asset's toshellJs.board.tsstill exports the very rules the stylesheet serves.Not done: importing
src/http/server.tsstill 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 --noEmitanddesign-lintpass, and 453 tests across 30 files that touch the changed modules pass.scripts/fast.tsnamed 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, orbun -e 'const t=performance.now(); await import("./src/web/style.ts"); console.log(performance.now()-t)'.🤖 Generated with Claude Code
Closes #8311