1.16.0 — 2026-08-11
Version 1.16.0 is a quality-and-performance release. A full audit of the framework's three hot paths — the reactive core, the DOM core, and the view layer — drove a series of measurable speed-ups and surfaced several real correctness bugs, all fixed here. The release also folds in the previously staged follow-up to the 1.15.1 security review. It contains no breaking changes and no module status transitions; the only new API is the additive trailing option on watchThrottle.
TIP
This page groups the changes by theme. The full list lives in the CHANGELOG. For how each affected API is meant to be used, follow the module guides linked from each section.
Headlines
- Batching that actually spans the propagation —
batch()now coalesces transitive updates too; diamond dependencies inside a batch trigger their effect once instead of once per branch. - Computeds only wake subscribers on real changes — a
computedwhose recomputed value isObject.is-equal to the last observed one no longer notifies downstream effects at all. - Cheaper hot paths across the board — signal writes, dependency tracking, DOM collection methods, and directive updates shed their per-call allocations.
- A batch of real bugs fixed — the
undelegate()no-op leak,bq-modelcaret resets while typing, directives binding to discardedbq-fortemplates, and more. - 1.15.1 follow-up landed — the residual
with-scope evaluator escape, thedeepCloneprototype-pollution guard, and the compiler numeric-literal fix ship in this release.
Reactive core
batch()keeps the batch open while flushing, so signal writes performed by observers keep coalescing into the same flush. Re-queued observers drain in follow-up passes (bounded at 100, mirroring the cyclic-effect guard).Computedre-validates before waking subscribers and skips notification when the value is unchanged — in the micro-benchmark, acomputed(() => count.value > 5)under 20k writes went from 20k effect runs to 2.- Allocation cuts: signal writes with zero or one subscriber no longer allocate a snapshot array (~6× faster with no subscribers), repeat reads of the same source inside one observer skip the dependency bookkeeping (~1.6× faster), computed chains propagate ~1.7× faster, and
effect()no longer allocates an inspectionSymbolwhen inspection is disabled. - Fixed: nested
batch()calls could execute observers twice per flush; a throwingcomputedwas left marked clean and served its stale cached value (it now stays dirty and retries). - New:
watchThrottle(source, callback, interval, { trailing: true })delivers the last value of a burst once the interval elapses. The default remains leading-edge-only. See the Reactive guide.
DOM core
- Fixed:
undelegate()called on a fresh wrapper (the documented usage) was a silent no-op that permanently leaked the delegated listener — the handler registry is now module-level and keyed by element, anddelegate()attaches a single listener per (element, event, selector, handler), counting registrations so one owner'sundelegate()cannot detach a delegation another owner still holds. The delegated dispatcher also no longer throws on non-Elementevent targets. - Fixed:
wrap(element)over a multi-element collection cloned the wrapper after the first element had been moved into it, so later wrappers contained copies of previously wrapped elements. - Cheaper:
replaceWith(string)sanitizes and parses once and clones per element;empty()usesreplaceChildren()(no HTML parser, no Trusted Types sink);children()/siblings()/index()avoid materializing sibling arrays;unwrap()is a single DOM mutation;css(object), form serialization, anddata()shed per-element allocations. See the Core guide.
View layer
- Fixed: directives declared before
bq-foron the same element were bound against the discarded template element, leaking a live effect that errored on every update —bq-foris now dispatched first regardless of attribute order. - Fixed:
bq-once/bq-memo/bq-initsilently subscribed the enclosingbq-forreconciler to signals they read; their evaluation is now untracked, matching their documented contract. - Fixed:
bq-modelre-wroteinput.valueon the effect tick its owninputevent triggered, resetting the caret while typing. - Fixed: children of
bq-html/bq-html-safecontent kept live effects after the first re-render replaced the markup. - Cheaper:
bq-class/bq-style/bq-ariaparse their static object expression once at bind time;bq-ifresolves its transition config only on an actual visibility flip;bq-text/bq-bind/bq-model/bq-htmlskip unchanged writes; directive parsing is memoized and expression evaluation caches its sandbox proxies per context. See the View guide.
Motion, store & compiler
onReducedMotionChangere-binds to the currentwindow.matchMediawhen subscribing and flushes preference changes that happened without achangeevent, so listeners and new-subscriber baselines stay accurate.deepClone(used by$patchDeep) special-cases only the genuinely dangerous__proto__key; own data properties merely namedconstructororprototypeare copied normally again instead of being silently dropped.- The view compiler rejects legacy leading-zero decimal literals (
007,01.5) instead of emitting strict-modeSyntaxErrors. - Security: the
with-scope evaluator hardening from 1.15.1 closes a residual member-access escape (items.constructor.constructor('…')()) via the new sharedhasDangerousMemberAccess()guard, applied to both the runtime evaluator and the ahead-of-time compiler.
Migration notes
There are no breaking changes in 1.16.0. Upgrade in place.
Behavioral notes, all strictly less-surprising than before:
- Effects that depended on being re-run when a computed's dependencies changed without the computed's value changing will now run less often. This matches the documented "recompute when dependencies change" contract; if you relied on the extra runs, read the underlying signal directly.
delegate()no longer stacks duplicate listeners for the same (element, event, selector, handler) tuple.
Engines
Publish and local validation target Node.js ≥ 24.0.0 and Bun ≥ 1.3.13. See Supported Runtimes.