Architecture
TypeScript (consumer)
│
│ synchronous JSI call, no bridge, no JSON serialization
▼
Nitro Module (C++ binding layer)
│
├──► Lexbor (C99)
│ ├── WHATWG-compliant HTML parsing
│ ├── In-memory DOM tree
│ ├── querySelector / querySelectorAll
│ ├── getAttribute / setAttribute
│ ├── textContent / innerHTML
│ └── serialize() → HTML string
│
└──► QuickJS (C)
├── Isolated JS runtime per instance
├── window / document stubs
├── DOM bindings → delegates to Lexbor
├── setTimeout / setInterval (own event loop)
├── console.log → forwarded to RN
└── Executes arbitrary user scripts
Why these technologies?
| Layer | Choice | Reason |
|---|---|---|
| Native binding | Nitro Modules | Synchronous, type-safe JSI bindings. Zero bridge overhead |
| HTML + DOM | Lexbor | Fastest WHATWG-compliant HTML parser in C99, zero dependencies |
| JS Engine | QuickJS | Built for embedding. Lightweight, fully isolated, ES2023 support |
| Glue | C++ | Connects QuickJS ↔ Lexbor ↔ Nitro/JSI |
Why not reuse the Hermes instance React Native already uses?
The Hermes runtime is a single shared instance, not designed for multiple isolated environments. QuickJS was built specifically for embedding and isolation, so each JSDOM.create() call gets its own independent runtime.