Repository navigation
Zero-GC signals: is 5-6x creation cost the floor for pre-allocated pools? #3298
PeshoVurtoleta
started this conversation in
Show and tell
Replies: 0 comments
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Uh oh!
There was an error while loading. Please reload this page.
Mr. Carniato — your bio says “give me signals.” So: signals.
Mr. Carniato, Solid Js developers,
I’m Zahary Shinikchiev. I built @zakkster/lite-signal (https://github.com/PeshoVurtoleta/lite-signal): a reactive core with zero transient allocation on the hot path — pre-allocated node and link pools, versioned lazy-pull propagation, no per-update Set, array, or work stack anywhere. Still glitch-free, still lazy. As far as I can find, it’s the only object-pooled zero-GC engine in the field that gives up neither.
On Andrii Volynets’ js-reactivity-benchmark (https://volynetstyle.github.io/js-reactivity-benchmark/) it places 4th of 15 — fair warning, that page still runs an older release. The current engine’s full run log is here: https://github.com/PeshoVurtoleta/lite-signal/blob/main/bench/AndriiVolynetsReactiveBench.log
Mr. Volynets also ran his own 100k-creation breakdown against my source and shared my write-up on it: https://www.linkedin.com/posts/zahary-shinikchiev_javascript-webperformance-softwarearchitecture-activity-7462880873458163712-6eto
Creation time is where I hit the stubborn wall: 7-9x behind the fastest engines. On his advice I added a boxed, non-callable API — shared prototype methods instead of per-signal closures — which brought the gap down to 5-6x (shipped in 1.5.0: https://www.npmjs.com/package/@zakkster/lite-signal). The rest looks structural: every node and link comes from a strict pre-allocated pool, so creation pays for manual property initialization and closure wiring that other engines hand to V8’s nursery allocator for free.
I understand what the zero-GC contract costs here, and how the top reactive frameworks sidestep it — letting V8’s bump allocator build nodes and closures and accepting nursery-rate GC as a fair trade. For lite-signal’s target deployments (Twitch Extensions, sub-16ms frame budgets, low-end hardware where a stop-the-world pause is the worst kind of jank), I’ve consciously chosen the other side of that tradeoff. I’d like to close more of the creation gap without breaking the public API contract — and if your read is that 5-6x is essentially the floor for any honest pooled design, I’ll take that as a clear answer.
Meanwhile I’ve pushed the effort into propagation, dynamic topology, and update latency, where I still see real headroom. Here’s the ledger of 20+ approaches that didn’t survive testing, and why: https://github.com/PeshoVurtoleta/lite-signal/blob/main/LEDGER.md — and I’m running low on new ones. If you have a few minutes, any advice or architectural pattern you could point me at would be deeply appreciated.
P.S. One related thing, briefly. Measuring “zero GC” honestly turned out to be its own problem, so I built @zakkster/lite-gc-profiler (https://github.com/PeshoVurtoleta/lite-gc-profiler). A two-point heapUsed delta doesn’t measure retention — it measures whether the tool is warm yet; a no-op workload read multiple KB per op. So it forces GC boundaries between a warmup phase and a steady phase, reads the compacted live-set floor at each anchor, and returns three verdicts instead of two: pass, fail, inconclusive. CI can tell “no violation” apart from “cannot tell.”
— Zahary Shinikchiev
All reactions