How it works
Ghostwire’s synthesizer runs a five-phase pipeline against the live DOM, every time a host needs a fresh skeleton:
walk(host) ├─ 1. Collect → traverse the subtree, applying .ignore/.keep exclusions ├─ 2. Classify → assign a BoneType to every eligible node (text, heading, media, control, container, icon) ├─ 3. Measure → one batched layout/style read pass — never interleaved with writes ├─ 4. Reduce → detect repeated siblings, fuse adjacent bones, cap node count └─ 5. Emit → a Bone Tree in coordinates relative to the hostRead/write separation is load-bearing. Every layout read (getBoundingClientRect, getComputedStyle) happens before any DOM write — mixing them forces a synchronous reflow per element instead of one batched reflow for the whole host. This is the single biggest performance invariant in the codebase, and it’s enforced by a regression-guarding test, not just convention.
Repeat-sibling sampling measures the first three structurally-uniform siblings in a run for real, then clones bones for the rest — preserving real count and spacing without re-measuring hundreds of near-identical rows. Text bones are always measured for real, never cloned, because per-row text width genuinely varies.
Signature-based memoization hashes the classified candidate set (including repeat-run item count) so an unchanged host skips synthesis entirely on the next cycle, invalidated by a ResizeObserver watching border-box size.
Adaptive freeze degrades a host from synthesize to freeze mode automatically after two consecutive slow (>50ms) syntheses — a single slow sample is treated as GC/engine jitter, not a structural signal.
Try the algorithm directly, live, against your own markup, in the playground.