@miaskiewicz/turbo-dom 0.1.55 → 0.1.57

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -14,7 +14,7 @@ npm install -D @miaskiewicz/turbo-dom
14
14
 
15
15
  - ✅ **More compatible than happy-dom** — 99.72% on html5lib-tests vs happy-dom's 37%.
16
16
  Runs React Testing Library, `user-event`, downshift, Radix UI, and Headless UI unmodified.
17
- - ⚡ **Faster than both** — ~130× jsdom / ~45× happy-dom on a realistic suite (parse-memoized repeated shells), ~8–35× faster HTML parsing on real pages, and ~2.2× happy-dom on repeated queries while staying 99.7% spec-correct.
17
+ - ⚡ **Faster than both** — ~130× jsdom / ~45× happy-dom on a realistic suite (parse-memoized repeated shells), ~8–44× faster HTML parsing on real pages, and ~2.7× happy-dom on repeated queries while staying 99.7% spec-correct.
18
18
  - 🎯 **Honest, not lying** — no fake layout numbers; `getBoundingClientRect()` is zeros.
19
19
  `getComputedStyle` runs a **partial cascade**: it resolves real injected `<style>` rules
20
20
  (emotion/MUI `.css-HASH{…}`) + inline styles with proper specificity/source order — but only
@@ -109,16 +109,16 @@ the suite row (ms/file, lower = faster):
109
109
 
110
110
  | benchmark | turbo-dom | happy-dom | jsdom |
111
111
  |---|---:|---:|---:|
112
- | **realistic suite**, 200 files (ms/file) | **~0.025** | 1.13 | 3.40 |
113
- | **cold per-file construct + query** (ops/s) | **~200k** | 579 | 266 |
114
- | **parse 56 KB SSR** (ops/s) | **510** | 48 | 66 |
115
- | **parse 20 KB real page** (ops/s) | **3,628** | 161 | 105 |
116
- | repeated query throughput (iters/s) | **~1.5M** | 680k | 3.3k |
112
+ | **realistic suite**, 200 files (ms/file) | **~0.025** | 1.08 | 3.20 |
113
+ | **cold per-file construct + query** (ops/s) | **~230k** | 575 | 267 |
114
+ | **parse 56 KB SSR** (ops/s) | **528** | 48 | 65 |
115
+ | **parse 20 KB real page** (ops/s) | **4,353** | 135 | 99 |
116
+ | repeated query throughput (iters/s) | **~1.8M** | 661k | 3.3k |
117
117
  | html5lib conformance | **99.72%** | 37.35% | 97.03% |
118
118
 
119
119
  On a realistic suite — 200 files of construct + queries + events — turbo-dom is
120
- **~45× happy-dom and ~130× jsdom**, runs repeated queries **~2.2× happy-dom**
121
- (~460× jsdom), and parses real pages/SSR documents **~8–35×** faster, all at 99.7%
120
+ **~45× happy-dom and ~130× jsdom**, runs repeated queries **~2.7× happy-dom**
121
+ (~550× jsdom), and parses real pages/SSR documents **~8–44×** faster, all at 99.7%
122
122
  conformance.
123
123
 
124
124
  The per-file setup number is so high because the parser **memoizes the read-only
package/index.d.ts CHANGED
@@ -37,11 +37,8 @@ export interface JsSoa {
37
37
  packed: Uint8Array
38
38
  n: number
39
39
  m: number
40
- tagNames: Array<string>
41
- attrNames: Array<string>
42
- attrPrefixes: Array<string>
43
- attrValues: Array<string>
44
- strings: Array<string>
40
+ strBlob: Uint8Array
41
+ strMeta: Uint32Array
45
42
  }
46
43
  /** Parse a document into the SoA flat buffer (the fast runtime path). */
47
44
  export declare function parseBuffer(html: string): JsSoa
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@miaskiewicz/turbo-dom",
3
- "version": "0.1.55",
3
+ "version": "0.1.57",
4
4
  "description": "Faster, more spec-correct DOM for test runners — native html5ever (Rust/WASM) parser + lazy copy-on-write DOM. A drop-in-style alternative to jsdom/happy-dom for vitest & jest.",
5
5
  "license": "MIT",
6
6
  "main": "index.js",
package/src/lib.rs CHANGED
@@ -82,11 +82,14 @@ mod napi_front {
82
82
  pub packed: Uint8Array,
83
83
  pub n: u32,
84
84
  pub m: u32,
85
- pub tag_names: Vec<String>,
86
- pub attr_names: Vec<String>,
87
- pub attr_prefixes: Vec<String>,
88
- pub attr_values: Vec<String>,
89
- pub strings: Vec<String>,
85
+ // The five string tables (tag_names, attr_names, attr_prefixes, attr_values,
86
+ // strings) are shipped as ONE raw UTF-8 byte blob + a meta array, NOT as
87
+ // Vec<String>. Vec<String> forces napi to UTF-8→UTF-16 convert every string
88
+ // at the boundary (~12% of parse, even for strings a test never reads). The
89
+ // blob crosses with zero conversion; JS decodes once, lazily, in unpack().
90
+ // str_meta = [count0..count4, then every string's byte length in table order].
91
+ pub str_blob: Uint8Array,
92
+ pub str_meta: Uint32Array,
90
93
  }
91
94
 
92
95
  impl From<core::Soa> for JsSoa {
@@ -104,15 +107,27 @@ mod napi_front {
104
107
  for v in s.attr_count.iter() { buf.extend_from_slice(&v.to_le_bytes()); }
105
108
  buf.extend_from_slice(&s.node_type);
106
109
  buf.extend_from_slice(&s.ns);
110
+ // pack all five string tables into one byte blob + meta (5 counts, then
111
+ // per-string byte lengths in table order). Zero napi string conversion.
112
+ let tables: [&Vec<String>; 5] =
113
+ [&s.tag_names, &s.attr_names, &s.attr_prefixes, &s.attr_values, &s.strings];
114
+ let total_strs: usize = tables.iter().map(|t| t.len()).sum();
115
+ let total_bytes: usize = tables.iter().flat_map(|t| t.iter()).map(|x| x.len()).sum();
116
+ let mut str_blob: Vec<u8> = Vec::with_capacity(total_bytes);
117
+ let mut str_meta: Vec<u32> = Vec::with_capacity(5 + total_strs);
118
+ for t in &tables { str_meta.push(t.len() as u32); }
119
+ for t in &tables {
120
+ for x in t.iter() {
121
+ str_meta.push(x.len() as u32);
122
+ str_blob.extend_from_slice(x.as_bytes());
123
+ }
124
+ }
107
125
  JsSoa {
108
126
  packed: Uint8Array::new(buf),
109
127
  n: n as u32,
110
128
  m: m as u32,
111
- tag_names: s.tag_names,
112
- attr_names: s.attr_names,
113
- attr_prefixes: s.attr_prefixes,
114
- attr_values: s.attr_values,
115
- strings: s.strings,
129
+ str_blob: Uint8Array::new(str_blob),
130
+ str_meta: Uint32Array::new(str_meta),
116
131
  }
117
132
  }
118
133
  }
@@ -19,13 +19,30 @@ export function unpack(soa) {
19
19
  const attrCount = new Uint16Array(ab, off, n); off += n * 2;
20
20
  const nodeType = new Uint8Array(ab, off, n); off += n;
21
21
  const ns = new Uint8Array(ab, off, n); off += n;
22
+ // Decode the five string tables from the raw byte blob (one pass per string),
23
+ // done ONCE here per cached parse. The native side ships bytes with no per-string
24
+ // napi UTF-8→UTF-16 conversion; the accessors below read plain arrays (hot path
25
+ // unchanged). meta = [5 counts, then every string's byte length in table order].
26
+ const meta = soa.strMeta, blob = soa.strBlob;
27
+ let mi = 5, bo = 0;
28
+ const table = (count) => {
29
+ const arr = new Array(count);
30
+ for (let k = 0; k < count; k++) {
31
+ const len = meta[mi++];
32
+ arr[k] = len === 0 ? '' : STR_DECODER.decode(blob.subarray(bo, bo + len));
33
+ bo += len;
34
+ }
35
+ return arr;
36
+ };
37
+ const tagNames = table(meta[0]), attrNames = table(meta[1]), attrPrefixes = table(meta[2]),
38
+ attrValues = table(meta[3]), strings = table(meta[4]);
22
39
  return {
23
40
  nodeType, ns, tagId, parent, firstChild, nextSib, textId, pubId, sysId,
24
41
  attrStart, attrCount, attrNameId, attrValueId, attrPrefixId,
25
- tagNames: soa.tagNames, attrNames: soa.attrNames, attrPrefixes: soa.attrPrefixes,
26
- attrValues: soa.attrValues, strings: soa.strings,
42
+ tagNames, attrNames, attrPrefixes, attrValues, strings,
27
43
  };
28
44
  }
45
+ const STR_DECODER = new TextDecoder();
29
46
 
30
47
  export class Buffer {
31
48
  constructor(soa) {
Binary file
Binary file