Issie logo Issie

Build Speed and Fable Caching

How the dev build gets fast startup, and what silently makes it slow again.

The scripts

Every one of these reaches scripts/start.js, which frees the two ports it needs before using them (scripts/free-port.js): 8672 for the webpack dev server and 9222 for the DevTools protocol that scripts/inspect-canvas.js talks to. A session interrupted in any of the usual ways leaves both held, and the failure that follows is worse than it sounds: the port is bound after a full Fable compile and after Electron has been told to open, so the window appears, stays blank, and the log says ready. A stale Electron on 9222 is quieter still — inspect-canvas connects to it and reports the previous run's canvas. Whoever holds the port is killed, whatever it is; that is a more reliable test than matching command lines.

A fable watch holds no port, so nothing above reaches it — and it is the leftover that matters most, because it is still watching: it recompiles on the next file change and can flip the tree's build mode under whoever runs the app next. Every entry point above therefore starts by removing the watchers an interrupted session left (scripts/free-watchers.js).

That check has to be nearly free, since it runs before a dev:once that is otherwise instant on an unchanged tree, and Windows cannot list processes without spawning PowerShell — 250ms before it has done anything, 640ms for the query. So a watch session leaves a note in build-fable saying it started and who owned it, and removes the note when it ends properly. No note, or a note whose owner is still alive, means there is nothing to look for: about 10ms, and the usual answer. Only a note left by a session that was killed is worth the listing, and then a leftover is identified by its PARENT being gone rather than by its pid — the shell dev.js spawns it through dies with the session while the Fable process under it does not, which is the whole problem.

scripts/clean-dev.js is still the tool for sweeping a whole abandoned session on request. It matches command lines rather than parentage, so it catches a watcher started some other way — and it will kill a running app, which is why it is a command you run and not something that happens at startup.

How Fable decides it can start fast

Fable has no on-disk cache of typed ASTs: a cold compile type-checks every file of the Renderer (~200 including dependencies) and takes on the order of a minute. What it does have:

  1. Project cracking cache (build-fable/<project>/fable_modules/project_cracked.json) — restoring project options takes ~200ms instead of several seconds.
  2. Up-to-date detection — if every generated .fs.js is strictly newer than its .fs, a one-shot compile is skipped entirely, and fable watch runs its --run command immediately (recompiling silently in the background to build its watch graph). dev.js uses that --run hook (scripts/fable-ready.js) as the signal to start Electron, so an unchanged tree starts the app in seconds either way.

What breaks it

If the cache seems wedged, dotnet fable clean in the project directory removes the generated files and caches for a genuinely fresh start.

Type something to start searching.