Repository navigation
P 13 Engine Start Steps
Context: After P-07, a warm start loads the engine entirely from the HTTP cache (3 KB transferred), yet it was ready no sooner than a cold start: about 1.6 s against a 1 s warm budget. There was no measure of where that time went. DuckDB-wasm 1.32.0's workers also stream the module through a progress counter into a new Response before compiling it, and Chromium only caches compiled WebAssembly for a response that came from fetch() itself, so the compiled engine was never cached.
Hypothesis: Letting Chromium cache the compiled module, and then starting the engine alongside the app shell, brings warm engine-ready under 1 s.
-
Step marks. User Timing marks record each step of starting the engine, and
time-to-engine-readyreports how long each took:engineBootStartMs,workerMs,instantiateMs,openMs,connectMsandreadyMs. -
Compiled-engine cache. The production build rewrites DuckDB-wasm's eh and mvp workers to compile
fetch()'s own response (duckdbWorkers()insrc/engine/vite-plugin.ts).- Each rewritten worker is emitted with its own content hash.
- The build fails if the code it replaces is missing, so an upgrade can't silently undo it.
- Blinkql never used the progress events.
Method: Reference laptop (i5-8250U, Windows 11, Chromium 156). time-to-app-shell and time-to-engine-ready, cold and warm, 3 warm-up and 10 measured iterations. The branch ran first and main (86bfbf9) straight after. Warm mode uses a persistent profile, as since P-07. The cache itself was checked separately: a persistent Chromium profile ran queries on each load and held the page open for 6 s, and SiteIsolatedCodeCache.WASM.Behaviour was read after every load.
| Metric | Before (median / p95) | After (median / p95) | Change (median) |
|---|---|---|---|
| timeToAppShell, cold | 396.2 / 493.1 ms | 351.2 / 454.2 ms | within noise |
| timeToAppShell, warm | 123.5 / 171.3 ms | 109.2 / 147.1 ms | within noise |
| timeToEngineReady, cold | 2169.2 / 2322.0 ms | 2066.9 / 2262.0 ms | within noise |
| timeToEngineReady, warm | 1444.7 / 1686.0 ms | 1362.7 / 1531.9 ms | within noise |
Where the time goes, on the branch (median / p95, ms):
| Step | Cold | Warm |
|---|---|---|
Navigation start until the engine starts (engineBootStartMs) |
413.0 / 458.4 | 138.8 / 165.0 |
Choosing the bundle, creating the worker (workerMs) |
6.1 / 8.4 | 4.9 / 5.9 |
The worker's script, then fetching, compiling and instantiating the module (instantiateMs) |
1410.5 / 1567.5 | 957.8 / 1096.3 |
Opening the database (openMs) |
175.7 / 189.1 | 181.7 / 231.8 |
Version check and connection (connectMs) |
1.3 / 1.5 | 1.2 / 1.4 |
Start-up settings and the first query (readyMs) |
50.4 / 58.1 | 46.7 / 63.1 |
The cache after sessions that ran queries (ms):
| Load | Code cache | instantiate | open | ready |
|---|---|---|---|---|
| 1 | miss, then written | 1289 | 169 | 41 |
| 2 | hit | 984 | 43 | 10 |
| 3 | hit | 970 | 71 | 11 |
| 4 | hit | 1041 | 45 | 9 |
Without the change, every load was a miss and nothing was written.
Findings:
-
The engine now caches, but only after real work.
- After a session that ran queries, later starts load the compiled engine from the cache: opening the database drops from about 170 to 45 ms, and the first query from about 45 to 10 ms.
- V8 only writes the cache once enough optimised code exists and compiling has been quiet for 2 s. Start-up alone never gets there, so the benchmark's warm runs, which only start the engine, don't change.
-
Compiling was not the main cost. A trace of a warm
instantiateMs(about 1 s) splits roughly into:- the worker's 770 KB script, about 180 ms;
- deserialising the cached module, about 370 ms;
- instantiating it, about 160 ms;
- DuckDB's own initialisation, about 390 ms.
A cache hit trades compiling for hashing and deserialising, so
instantiateMsbarely moves. -
The remaining ideas can't reach 1 s.
- Starting the engine before the first paint would save at most
engineBootStartMs, about 140 ms warm. - Folding the start-up queries would save at most
readyMs, about 45 ms. - Together they would leave about 1.15 s.
- Starting the engine before the first paint would save at most
Trade-offs:
- The build now rewrites two files from a dependency. The rewrite is one expression, checked at build time and covered by a unit test.
- The dev server serves the workers unchanged, so the cache only applies to production builds.
Evidence: raw results in bench/results/2026-10-27T04-32-31Z-86bfbf9.json (after) and bench/results/2026-10-27T04-34-05Z-86bfbf9.json (before). A Chrome trace shows v8.wasm.moduleCacheHit for duckdb-eh-*.wasm on loads after a session that ran queries.
Conclusion: Keep both changes. The step marks show where start-up time goes, and the cache makes returning sessions faster at opening the database and answering their first query. The hypothesis doesn't hold: about 1.3–1.4 s of a warm start is instantiating and initialising DuckDB, which no cache removes. The warm engine-ready budget is now 1.5 s (see Performance budgets). Going lower would need a smaller engine build.
⚡ Blinkql: queries in the blink of an eye · MIT licensed · Live app · Issues · Discussions
Project
Engineering
- Tech Stack
- Architecture
- Performance & Benchmarks
- Decision Records (ADRs)
- Privacy & Security
- Browser Support & Limitations
Design
Community