Performance
Assets are served with an ETag and answer Range requests. Neither is an optimisation: DuckDB reads remote files by range, so returning the whole file each time makes a 4MB catalog cost 4MB per read, thousands of times for one page. Without revalidation the 36MB DuckDB binary is re-downloaded on every load.
Even so, a ducklake catalog read through httpfs in wasm is slow — a long sequence of small requests. It works, but it is slow enough that gating an app that does it under sandboxed I/O is impractical.
Where a client render’s time goes
Measured on a ducklake-backed app’s detail page, warm, in Chromium. A warm render makes no network requests at all — the cost is entirely compute, and almost none of it is the transport between the worker and the page.
| message round trip to the worker | ~1ms |
| Arrow batches → strings | ~2ms |
| the app’s own query | the rest, tens of ms |
So the framework’s overhead per render is a few milliseconds. What dominates is the app’s SQL, and two properties of DuckDB decide how much:
Every reference to a ducklake table costs milliseconds to bind. One reference is ~7ms, five are ~23ms, against ~0.9ms and ~2ms for a plain table. An app reading a ducklake table from several branches pays that on every request, before touching any data. Materialising into a local table at boot removes it.
A macro is bound in full, every branch of it, on every request. Binding a ~30-branch macro costs ~25ms even for a request matching no route at all. Prepared statements do not avoid this: duckweb_match depends on the parameters, so DuckDB re-binds on each execute — measured slower than a fresh query, 44ms against 30ms. Branches containing subqueries cost ~0.55ms each to bind against ~0.15ms for trivial ones.
Neither is a transport problem, and neither is fixed here — they are properties an app can design around, by keeping hot data in local tables and by keeping the number of branches in a route’s macro down.
A caution for anyone benchmarking this: figures taken on a loaded machine, or by timing one route repeatedly so cold-start lands on whichever ran first, came out wrong by 3-5x. Round-robin between routes at low load reproduces.