From stacksjs/stacks#3035. The Stacks admin dashboard (128 pages) is served only by serve in dev mode, with renderCache: true and renderCacheVary: 'source'. A page's first render costs 1.2 to 4.7 s; after that it comes from the cache in under 10 ms. The cache lives only in memory (htmlCache, a boundedCache), so every restart pays every page's first render again. Developers restart often: config changes, backend edits through the dashboard's worker restart, buddy dev cycles.
Prewarming isn't the answer here: it ran on the serving thread and made pages take 3 to 28 s while it ran (stacksjs/stacks#2964), so Stacks turned it off.
Why it isn't just "write htmlCache to disk"
An entry is checked by templateSignatureFresh, the mtimes of the entry template plus the dependencies that processDirectives recorded. That is enough within one process only because other inputs are handled by clearing the whole cache in memory:
- Client bundles. Composables and functions under
watchDirschange the emitted HTML. The file watcher clears the cache, but they aren't in an entry's signature. - The CSS engine output and the
cssCssCache. These are cleared by the watcher too. - Process-lifetime inputs. These don't change while the process runs, but can between runs: the stx and bun-plugin-stx versions, the serve options (component, layout and partials dirs,
publicDir, domains), the user css config, andproduction. - The image warm-up state. Renders are only cached once
placeholdersAreReady.
A disk cache that skipped any of these would serve stale HTML after a change made while the server was down. That's worse than a slow first visit.
Proposal
Persist entries to the stx state dir (next to image-delivery) only under renderCacheVary: 'source', keyed by file path. Store each entry with:
- its template signature, as today;
- a process fingerprint: stx and bun-plugin-stx versions, a hash of the serve options that affect rendering, and a hash of the user css config;
- a watch fingerprint: the mtimes of every file under
watchDirs, or a hash of the listing.
Load lazily. An entry is used only if all three still match, and anything that doesn't match is discarded. The in-memory watcher behaviour stays as it is.
Open questions
- Is there an input missing from the list above? The full set of what goes into a render is the thing to get right.
- Should it be opt-in (
renderCachePersist: true) at first?
Measurements and context: stacksjs/stacks#3035. Related: 1f04af9, which took the image warm-up scope scan from 49.5 s to under 1 s on the same dashboard.