Skip to content

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 propagationbatch() 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 computed whose recomputed value is Object.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-model caret resets while typing, directives binding to discarded bq-for templates, and more.
  • 1.15.1 follow-up landed — the residual with-scope evaluator escape, the deepClone prototype-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).
  • Computed re-validates before waking subscribers and skips notification when the value is unchanged — in the micro-benchmark, a computed(() => 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 inspection Symbol when inspection is disabled.
  • Fixed: nested batch() calls could execute observers twice per flush; a throwing computed was 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, and delegate() attaches a single listener per (element, event, selector, handler), counting registrations so one owner's undelegate() cannot detach a delegation another owner still holds. The delegated dispatcher also no longer throws on non-Element event 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() uses replaceChildren() (no HTML parser, no Trusted Types sink); children()/siblings()/index() avoid materializing sibling arrays; unwrap() is a single DOM mutation; css(object), form serialization, and data() shed per-element allocations. See the Core guide.

View layer

  • Fixed: directives declared before bq-for on the same element were bound against the discarded template element, leaking a live effect that errored on every update — bq-for is now dispatched first regardless of attribute order.
  • Fixed: bq-once/bq-memo/bq-init silently subscribed the enclosing bq-for reconciler to signals they read; their evaluation is now untracked, matching their documented contract.
  • Fixed: bq-model re-wrote input.value on the effect tick its own input event triggered, resetting the caret while typing.
  • Fixed: children of bq-html/bq-html-safe content kept live effects after the first re-render replaced the markup.
  • Cheaper: bq-class/bq-style/bq-aria parse their static object expression once at bind time; bq-if resolves its transition config only on an actual visibility flip; bq-text/bq-bind/bq-model/bq-html skip unchanged writes; directive parsing is memoized and expression evaluation caches its sandbox proxies per context. See the View guide.

Motion, store & compiler

  • onReducedMotionChange re-binds to the current window.matchMedia when subscribing and flushes preference changes that happened without a change event, so listeners and new-subscriber baselines stay accurate.
  • deepClone (used by $patchDeep) special-cases only the genuinely dangerous __proto__ key; own data properties merely named constructor or prototype are copied normally again instead of being silently dropped.
  • The view compiler rejects legacy leading-zero decimal literals (007, 01.5) instead of emitting strict-mode SyntaxErrors.
  • Security: the with-scope evaluator hardening from 1.15.1 closes a residual member-access escape (items.constructor.constructor('…')()) via the new shared hasDangerousMemberAccess() 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.

Released under the MIT License.