@miaskiewicz/turbo-dom 0.1.55 → 0.1.56
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 +8 -8
- package/index.d.ts +2 -5
- package/package.json +1 -1
- package/src/lib.rs +25 -10
- package/src/runtime/buffer.mjs +19 -2
- package/turbo-dom-parser.darwin-arm64.node +0 -0
- package/turbo-dom-parser.linux-arm64-gnu.node +0 -0
- package/turbo-dom-parser.linux-x64-gnu.node +0 -0
- package/turbo-dom-parser.linux-x64-musl.node +0 -0
- package/turbo-dom-parser.win32-x64-msvc.node +0 -0
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–
|
|
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.
|
|
113
|
-
| **cold per-file construct + query** (ops/s) | **~
|
|
114
|
-
| **parse 56 KB SSR** (ops/s) | **
|
|
115
|
-
| **parse 20 KB real page** (ops/s) | **
|
|
116
|
-
| repeated query throughput (iters/s) | **~1.
|
|
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.
|
|
121
|
-
(~
|
|
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
|
-
|
|
41
|
-
|
|
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.
|
|
3
|
+
"version": "0.1.56",
|
|
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
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
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
|
-
|
|
112
|
-
|
|
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
|
}
|
package/src/runtime/buffer.mjs
CHANGED
|
@@ -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
|
|
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
|
|
Binary file
|
|
Binary file
|
|
Binary file
|