Files
buuh/docs/v8.md
T
Bendik Aagaard LynghaugandClaude Fable 5 c9f5ba2fda bench: @choojs/html vs nanohtml v1 vs µhtml v5, happy-dom + real Chromium
npm run bench (happy-dom, all three engines) and npm run bench:browser
(Playwright Chromium, the two ESM engines). Honest numbers recorded in
docs/v8.md: we create ~12% faster than µhtml in real Chromium (the
parse-once/clone design), µhtml updates in place ~5x faster than
fresh-tree + nanomorph (choo's architectural cost, mitigated by component
caching), and server strings are on par with nanohtml v1.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014NgfSjHE11oFpoSnLVKLXd
2026-09-08 17:59:53 +02:00

4.7 KiB

choo v8 — branch notes

This branch is the working tree for the v8 modernization effort. The v7 code at the repo root is untouched and stays authoritative until 8.0.0 ships; everything new lives under packages/.

Layout

  • packages/core@choojs/core: the Choo class, same API as choo v7 (use/route/start/mount/toString/emit), ported to ESM. The nano* internals are consolidated into lib/ as attributed ports:
    • lib/bus.js ← nanobus 4.5.0
    • lib/router.js ← nanorouter 4.0.0 + wayfarer 7.0.1 (+ trie)
    • lib/cache.js ← choo component/cache.js + nanolru 1.0.0
    • lib/raf.js ← nanoraf 3.1.0, lib/href.js ← nanohref 3.1.0
    • lib/timing.js ← nanotiming 7.3.1 (unified on global performance)
    • lib/dom.js ← document-ready 2.0.1 + scroll-to-anchor 1.0.0
    • lib/query.js — nanoquery replaced by URLSearchParams
  • packages/html@choojs/html: the rendering package.
    • server.js — server-side tagged template (← nanohtml 1.10.0 server, transform branches removed; pure runtime)
    • browser.js — runtime-only cached template tag: each template literal is parsed once (WeakMap keyed on its strings array) into a plus hole instructions; renders clone and fill. Values never pass through innerHTML. Document-level roots ( etc.) parse via DOMParser since drops them. Known limits: no dynamic tag names, no holes in raw-text elements, SVG fragments need their root, and (unlike v7) whitespace is preserved as authored — which is what makes server and browser output byte-identical.
    • morph.js — ← nanomorph 5.4.3, consolidated to one module
    • raw.js — mark pre-encoded strings (works with both renderers)
    • packages/component@choojs/component: ← nanocomponent 6.6.0 + on-load 3.4.1 as ES classes; the future island/hydration boundary.
    • examples/counter — the isomorphic proof: one app module, mounted zero-build in the browser via import map (index.html), string-rendered in Node (render.js).
    • Status vs the modernization plan

      • Phase 1: monorepo scaffold, ESM ports, choo v7 node test suite green on node --test (Node ≥ 24), CI on GitHub Actions
      • URL normalization fix: WHATWG URL parsing, single decode with raw fallback (no more URIError on '%'), NFC matching, per-segment wildcard decode, decoded state.href
      • Phase 2 (core): browser renderer rewrite, @choojs/component, zero-build counter example, full-app integration test in happy-dom
      • Phase 2 (tail): adoption-style hydration with mismatch warnings (@choojs/html/hydrate, wired into mount()), real-browser Playwright e2e (zero-build page, SSR-then-hydrate page, adoption proof; CI job included), benchmarks vs nanohtml v1 / µhtml

      Benchmarks (2026-09, 100-row table, npm run bench / bench:browser)

      Real Chromium: @choojs/html creates fresh trees ~12% faster than µhtml v5 (3.5k vs 3.1k ops/s) — the parse-once/clone design pays off in native DOM. µhtml updates in place ~5x faster than our fresh-tree + nanomorph loop (3.0k vs 0.6k ops/s): that is choo's architectural cost, mitigated in real apps by @choojs/component caching (proxy nodes skip unchanged subtrees), and the number to beat if Phase 3 explores keyed-hole optimizations. Server string rendering is on par with nanohtml v1 (~13k ops/s, within 6%). happy-dom numbers in bench/render.js are indicative only.

      • Phase 3: v8 wiring (toStream, devtools, codemod). Includes the answer to choojs/choo#653 (async/lazy routes): route handlers may resolve lazily via native import(); browser renders previous tree or a loading view until resolution, server awaits inside toStream (toString stays sync and fails loudly). Deliberately deferred until toStream exists so the SSR half ships with the browser half — the missing server story is what stalled #653. API shape (thenable handler vs lazy() wrapper vs loader-view option) goes to the RFC.
      • Phase 4: bankai v10 (Vite 8/Rolldown shell, SSR middleware, 103 Early Hints, service worker, precompression)
      • Phase 5: docs, examples, launch

      Deliberate changes from v7

      • choo() still works without new; Choo is also a named export.
      • nanoquery → URLSearchParams (same output shape, repeated keys → arrays).
      • Assertions live in lib/assert.js so production builds can strip them.
      • Everything targets Node ≥ 24 and Baseline Widely Available browsers; there is no compile step and no transpilation anywhere.

      Attribution

      All ported modules are MIT-licensed work by the original choojs/nano* authors and contributors; per-file headers note the source package and version. This branch exists to carry that work forward, not to replace it.