A performance release. No API changes; results are identical to 1.0.3 — verified by the full conformance suite and a 500k-input differential check on normalize-space.
Performance
The headline is an algorithmic rewrite of the query shape Capybara leans on, plus a round of allocation and per-node cuts. Found with a CPU profile (node --cpu-prof bench/bench.js).
//-fusion (the big one) —//Edesugars to/descendant-or-self::node()/child::E(XPath 1.0 REC §2.5), whose node-set is exactlydescendant::E. A new parse-time normalization fuses that step pair into a singledescendant::Estep, so the common.//Xshape now streams one tree walk instead of materializing the entiredescendant-or-self::node()set (text and comment nodes included) and then re-walking every node's children. The fusion is gated by a conservative position-stability guard: it never fires when a predicate can observe proximity position or size (a numeric/positional predicate, orposition()/last()), so//e[1],//e[last()], etc. keep their exact per-parent semantics.- Fewer allocations in the predicate loop —
applyPredicatesreuses one child context per frame and skips the output-array copy when a predicate keeps every candidate;unionNodeSetsshort-circuits an empty side;NodeSet.firstfast-paths the common single-node set. - Less per-node work — single-pass
normalize-spaceover the exact XPath whitespace set (#x20|#x9|#xD|#xA), with a fast path for already-normalized text; one Map lookup on thestring-valuememo hit path;*name tests skip the name/namespace adapter reads; structural classifications (single relative step,@name <op> literalshape, folded HTML attribute name) are memoized in WeakMaps keyed by the AST node, leaving the parse tree immutable.
Total profiled CPU on the benchmark dropped ~40% (1370ms → 827ms). Per query, on the capybara-simulated–shaped benchmark (node bench/bench.js, 200-block page), ~28–58% faster, with scaling still linear:
| query | 1.0.3 | 1.0.4 |
|---|---|---|
| count all elements | 0.081ms | 0.034ms |
| all fields (union-in-predicate) | 0.214ms | 0.134ms |
field by label (//label hoist) |
0.403ms | 0.241ms |
| deep descendant attr | 0.147ms | 0.078ms |
| link by text | 0.161ms | 0.076ms |
| table cell by text | 0.366ms | 0.265ms |
Full Changelog: v1.0.3...v1.0.4