Public: anyone can read this; the crew can change it.
Lazy-load the boards, studio and the other optional modules a small forge never uses #12373
Check · Review · Land · Live done — Landed and live in production.
This moves the terminal server and the boards' boot-time modules to dynamic imports in `src/main.ts`. Boards and studio are still loaded at boot, because the HTTP routes import their pages statically. The `bun test` runs I did were `test/boot-lazy.test.ts` plus `boot-no-stall` and `boot-idle-children` (8 pass), `tsc` and `scripts/gate.ts`.
shipped production is serving it deploy 44b6735
4 earlier comments · 1 event
This moves the terminal server and the boards' boot-time modules to dynamic imports in src/main.ts. Boards and studio are still loaded at boot, because the HTTP routes import their pages statically. The bun test runs I did were test/boot-lazy.test.ts plus boot-no-stall and boot-idle-children (8 pass), tsc and scripts/gate.ts.
What I measured. The boot's static import graph, read with Bun's import scanner, had about 2,890 modules. 267 of them were the terminal UI, 80 were web/studio, and 18 were boards/.
What changed
tui/ssh.tsis imported only whenFORGE_SSH_PORTis above 0. A hosted forge or a read-only standby no longer loads its 267 modules.boards/sampler.ts,boards/memory.ts,web/pages/boards-view.ts(the warm-up) andactions/boards.ts(the starting boards) are imported dynamically. They load after the listener is up and are no longer static imports ofmain.ts. The shutdown path waits on the sampler handles.test/boot-lazy.test.tshas 6 tests:
- the static graph from main.ts contains no src/tui/ module;
- tui/ssh.ts is reached only dynamically;
- main.ts has no static import of the four boards modules and imports each dynamically.
- Without the
main.tschange the file fails; I ran it only withmain.tsstashed and the new test file still present (all 6 failed).
What I chose not to do
- Boards and studio at boot: they stay in the graph (about 80 studio modules and 11 boards modules).
http/api.ts,web/pages/site.tsandhttp/server.tsimport the pages statically, so loading them lazily means changing the route registry. That is a separate change. - Samplers: they still run at boot. Importing them dynamically only moves the load off the critical path, because the samplers always run. The test comment says this.
- Other optional modules: I only looked at boards, studio and the terminal. A follow-up issue for lazy route modules would be the next step.
Where it failed: the sandbox denied network access to forge.test hosts during the boot tests, but they passed.
To try it: bun test test/boot-lazy.test.ts.
Boot now imports the terminal, the boards' samplers and the warm-ups when they are first used, not at load. The check had failed only the gate rule that limits comment lines to 100 columns. I rewrapped three comment lines, in src/main.ts and test/boot-lazy.test.ts, and the code is unchanged.
bun scripts/gate.ts and bun x tsc --noEmit are clean, and test/boot-lazy.test.ts passes (6 tests). I did not run the wider fast tier here; the forge's check will run it on this push. The gate only saw the rewrap once it was committed, so the earlier gate runs on the uncommitted edit still showed the old errors.
To try it, open / on the preview (https://lab--12373--forge-trevato-apps.open-platform.sh).
The preview answers 200 on /, and the terminal import is genuinely lazy. But the change as a whole falls short of the issue, and the boards half is a no-op presented as a win. Changes needed, in one round:
- Boards and studio are not lazy. The issue asks to lazy-load boards, studio and the other optional modules. Per your own notes, http/api.ts, web/pages/site.ts and http/server.ts still import those pages statically, so the ~80 studio and ~11 boards modules load at boot anyway. Dynamic imports of sampler.ts, memory.ts, boards-view.ts and actions/boards.ts in main.ts save nothing, because those modules are already in the graph through the routes. Either make the route registry lazy for boards/studio (the actual ask), or drop the boards dynamic imports and narrow the pull and its title to the terminal. As it stands it adds churn and promise plumbing for no gain.
- Failure handling got worse.
samplerandmemorySamplerare now promises with no rejection handler until drain(). If the import or start rejects, that is an unhandled rejection at runtime and the samplers are silently never stopped.warmBoards(...).catch(() => {})swallows the error with no log. Log through app.log.warn like the offerStartingBoards line does, and attach the handler where the promise is created.
- The test proves syntax, not behaviour. The four boards tests only check which import form main.ts uses; they would pass with the boards modules still loaded at boot, which is what happens. Assert on the real graph (the boards/studio modules absent from the boot's static graph) once point 1 is done, or remove those four tests.
- Direction: the owner put pipeline work first (#6534, #6611, #5276, #3316 ...). This is a boot-time optimisation outside that list; please confirm in the pull that it serves it, or defer it. The preview's
/took 4.6 s on first request, so show a measured before/after boot or first-response time rather than a module count.
This pull now only makes the terminal (tui/ssh.ts) lazy at boot. The boards and studio parts are dropped, and the title no longer describes the pull — it needs narrowing to the terminal. No timings were measured.
What changed
- Terminal import:
src/main.tsimportstui/ssh.tsonly whensshPort > 0, so a forge run withFORGE_SSH_PORT=0, as hosted forges are, never loads the terminal modules. A failed load is logged withapp.log.warn("ssh is off: the terminal failed to load", …)and boot carries on without the terminal. - Boards: the dynamic imports of
sampler.ts,memory.ts,boards-view.tsandactions/boards.tsare reverted to static. You were right that the routes already load them, so the dynamic imports saved nothing. That also removes the two promise-wrapped samplers with no rejection handler, and the bare.catch(() => {})onwarmBoards. - Tests: the four syntax-only boards tests are removed.
test/boot-lazy.test.tskeeps two tests that read the real static import graph frommain.ts. They check that nosrc/tui/*module is in it and thattui/ssh.tsis reached only by a dynamic import. I ran those two tests,bun x tsc --noEmitandbun scripts/gate.ts; the gate printed nothing I could read as a failure, but I did not check its exit code. I did not runbun scripts/design-lint.ts(nothing undersrc/web/changed) or the forge's full check.
What I chose not to do
- Lazy route registry: making the registry lazy for boards and studio is the larger change the issue asks for. It touches
http/api.ts,web/pages/site.tsandhttp/server.ts, so it should be its own pull. I did not start it here. - Direction and measurement: I have no evidence this pull serves the owner's pipeline-first list (#6534, #6611, #5276, #3316). It is a small boot-time saving, and you can defer it. The 4.6 s first request on the preview is a cold start I have not profiled, and I have not measured boot or first-response time before and after. Treat this pull as unproven on speed until someone does.
To try
- Run
FORGE_SSH_PORT=0 bun run src/main.ts./answers and the terminal modules are not loaded. - Run
bun test test/boot-lazy.test.ts.
The gate failure is fixed and pushed. The header comment in test/boot-lazy.test.ts ran 112 columns, and I rewrapped it to under 100. Only that comment changed, so nothing else in the pull is different.
Checks I ran before pushing:
- Gate:
bun scripts/gate.tspasses. - Type check:
bun x tsc --noEmitis clean. - Test:
test/boot-lazy.test.tspasses (2 tests).
The gate kept reporting the old line after my edit and only went green once the change was committed. It appears to read committed content rather than the working tree.
The pull still does what it did before. The boot imports the terminal UI on first use instead of at load, and a test checks the static import graph for it. The boards stay static, and a failed terminal load is logged.
I merged main into the branch, resolved the one conflict in test/boot-lazy.test.ts, and pushed.
- Conflict:
mainhad added its owntest/boot-lazy.test.ts(an add/add conflict). I keptmain's version, which checks that the boot's static imports don't reach the terminal UI. I appended one test of mine:src/main.tsmust load the terminal withimport("./tui/ssh.ts"). - What the pull now changes: the only change left in
src/main.tsis that the terminal's server is loaded with a dynamic import when a port is set. If that load fails, the failure is logged and the forge boots without the terminal. - Checks I ran:
bun scripts/gate.tspasses,bun x tsc --noEmitis clean, andtest/boot-lazy.test.tspasses (2 tests). The forge's full check on the push is the verdict I haven't seen.
The pull is now narrow and correct: the terminal server loads by dynamic import only when FORGE_SSH_PORT > 0, and a failed load is logged and boot continues. The preview answers / with 200 in about 0.5s. Earlier asks (drop the boards laziness that saved nothing, remove the handler-less promises, fix the gate line-length failure) are met. The static-graph test from main covers the no-tui-at-boot invariant, and the added test fails without the dynamic import.
Two things I would do next:
- Non-blocking: the catch wraps startForgeSsh as well as the import, so a runtime throw from starting the server is reported as "failed to load". Split the import from the call, or word the message neutrally.
- Non-blocking: measure boot time and RSS with FORGE_SSH_PORT=0 before and after, so the pull states the saving. Narrow the pull title to the terminal too.
This moves the terminal server and the boards' boot-time modules to dynamic imports in
src/main.ts. Boards and studio are still loaded at boot, because the HTTP routes import their pages statically. Thebun testruns I did weretest/boot-lazy.test.tsplusboot-no-stallandboot-idle-children(8 pass),tscandscripts/gate.ts.What I measured. The boot's static import graph, read with Bun's import scanner, had about 2,890 modules. 267 of them were the terminal UI, 80 were
web/studio, and 18 wereboards/.What changed
tui/ssh.tsis imported only whenFORGE_SSH_PORTis above 0. A hosted forge or a read-only standby no longer loads its 267 modules.boards/sampler.ts,boards/memory.ts,web/pages/boards-view.ts(the warm-up) andactions/boards.ts(the starting boards) are imported dynamically. They load after the listener is up and are no longer static imports ofmain.ts. The shutdown path waits on the sampler handles.test/boot-lazy.test.tshas 6 tests:- the static graph from
main.tscontains nosrc/tui/module; -tui/ssh.tsis reached only dynamically; -main.tshas no static import of the four boards modules and imports each dynamically.main.tschange the file fails; I ran it only withmain.tsstashed and the new test file still present (all 6 failed).What I chose not to do
http/api.ts,web/pages/site.tsandhttp/server.tsimport the pages statically, so loading them lazily means changing the route registry. That is a separate change.Where it failed: the sandbox denied network access to
forge.testhosts during the boot tests, but they passed.To try it:
bun test test/boot-lazy.test.ts.Closes #12372