tachyon-dom
v0.4.0
Published
A small TypeScript UI runtime and HTML-first compiler.
Maintainers
Readme
Tachyon DOM
Tachyon DOM is an experimental HTML-first compiler that turns static templates into direct DOM updates, using a small fine-grained runtime shared with SSR and streaming targets.
Experimental status: the compiler and runtime are ready for evaluation, examples, and incremental adoption, but public APIs may change before a stable release.
Why Tachyon DOM?
- HTML stays recognizable while dynamic fields become explicit DOM paths.
- Fine-grained updates write to cached text, attribute, class, and list targets without a virtual DOM.
- One parsed template feeds client, buffered SSR, and streaming server targets.
- Browser-safe runtime imports keep compiler and server dependencies out of client bundles.
Quick Example
This .td component combines a signal-backed counter with a keyed list:
<script setup lang="ts">
const count = createSignal(0);
const rows = createSignal([
{ id: 1, label: "Alpha" },
{ id: 2, label: "Beta" },
]);
const increment = (): void => count.update((value) => value + 1);
</script>
<main>
<button on:click={increment}>{count}</button>
<ul>
<for each={rows} key={row.id}>
<li>{row.label}</li>
</for>
</ul>
</main>The server targets escape interpolated text. The client target records direct paths to the button text and list container, then updates only those targets when their signals change.
What the Compiler Emits
The real output uses modular runtime imports and explicit cleanup ownership. Abbreviated, the hot path looks like this:
const countText = __tachyonTextAt(root, [0, 0]);
cleanups.push(__tachyonEffect(() => __tachyonSetText(countText, __tachyonRead(scope.count))));
cleanups.push(__tachyonEffect(() => __tachyonMountTextKeyedList(listRoot, [], __tachyonRead(scope.rows), listOptions)));
cleanups.push(() => __tachyonCleanupTextKeyedList(listRoot, []));Static markup remains in a reusable template. Events are attached once per created target, keyed rows retain their DOM nodes while moving, and the generated binding root disposes effects, resources, events, and controls together.
See the syntax specification for supported expressions and exact target behavior.
Predictable Cost
The compiler picks the smallest runtime path a region needs: text-only rows use runtime/list-text, rows with attributes or events use runtime/list, and simple branches use runtime/conditional-core instead of the generic conditional. The lightweight and generic paths differ in size only, never in observable behavior. To see which path a template got and why, ask the compiler:
tachyon-dom explain src/routes/index/page.td<for> under root.1 uses tachyon-dom/runtime/list.
The lightweight module was not chosen because:
- a row uses bind:value={row.label}
Runtime imports: tachyon-dom/runtime/text, tachyon-dom/runtime/event, tachyon-dom/runtime/list, tachyon-dom/runtime/signalThe same data is available as explainCompiledTemplate() from tachyon-dom/compiler and as tachyon-dom explain --json.
Measured Size
import { createSignal } from "tachyon-dom" bundles to 1243 bytes minified in the current esbuild contract.
Quick example client bundle: 17503 bytes minified, 6002 bytes Brotli, including the counter, text-only keyed-list binding code, and browser runtime. The generated list's cached DOM targets and lower-allocation update path increased this bundle by 816 minified and 236 Brotli bytes from the earlier measurement while improving representative warm updates. The development-only Inspector trace changed minifier output; the quick example is 32 minified and 7 Brotli bytes smaller than the previous baseline, while the minimal signal entry is 6 minified bytes larger. CI requires the minified baseline exactly, permits 1% Brotli variance across Node/zlib patch versions, enforces absolute 18000-byte minified and 6250-byte Brotli budgets, and rejects TypeScript, parse5, compiler, server, app, language-server, and runtime diagnostics modules from both browser metafiles. pnpm check:client-bundle-attribution records ten smaller fixtures, including a one-template and a thirty-template page whose difference is the per-template descriptor and glue cost, and a varied page of eight structurally different templates (forms, dynamic attributes, nested control flow, refs, richer expressions) that shows the per-template cost the repeated page understates, with the same build settings and keeps their esbuild contribution data and provenance in a unique ignored artifact.
pnpm build
pnpm check:browser-entry
pnpm check:quick-example-sizeThese numbers are produced from repository scripts, not estimated from source files. Subpath size budgets remain available through pnpm check:size.
Keyed List Benchmark
On this machine, Tachyon DOM performed within 3% of the comparison implementations across nine keyed-row operations.
| Compared with | Tachyon DOM result | | -------------------- | -----------------: | | vanillajs-lite-keyed | 1.8% faster | | solid-keyed | 0.9% faster | | marko-keyed | 2.5% slower |
Results are the geometric mean of nine operations: row creation, replacement, partial updates, selection, swapping, removal, append, clear, and creation of many rows. Lower execution time is better.
The recorded run used:
- AMD Ryzen 9 9950X
- Chromium 149.0.7827.55
- Production Vite builds
- 2 warmup runs and 7 measured runs per operation
- 20% trimmed means to reduce outlier influence
The complete JSON artifact includes raw samples, variability, dependency versions, and clean Git provenance.
Reproduce the same benchmark contract locally:
pnpm bench:localThis is a repository-local comparison based on the js-framework-benchmark row-operation model. It is not an official upstream result or a general ranking of framework performance. See the benchmark methodology for details.
Install
Create a route-local starter:
npm create tachyon-dom@latest my-app
cd my-app
pnpm install
pnpm devThe equivalent pnpm command is pnpm create tachyon-dom my-app. For an installed package, use tachyon-dom init --template basic --out my-app or select the ssr template.
The normal project shape keeps route markup in src/routes/**/page.td, client code in src/client/main.ts, and generated route registration in src/routes.generated.ts. Start with the getting-started guide.
Compiler and Runtime Boundaries
The package root contains browser-safe reactive and runtime APIs such as createSignal(), createMemo(), createResource(), createRoot(), onCleanup(), and the client router. Heavier tools use explicit subpaths:
import { createClientRouter, createSignal } from "tachyon-dom";
import { compileTemplate } from "tachyon-dom/compiler";
import { defineApp } from "tachyon-dom/app";
import { html } from "tachyon-dom/server/html";
import { tachyonDom } from "tachyon-dom/vite";This boundary keeps TypeScript, parse5, language-server, app, and server graphs away from a browser consumer that only needs reactivity.
Runtime-only consumers do not install TypeScript, OXC, or language-server tooling. Add the optional peers for the features you use:
pnpm add typescript oxc-parser
pnpm add vscode-languageserver vscode-languageserver-textdocumentTypeScript powers SFC script transformation and OXC parses advanced template expressions. The second command is only needed for tachyon-dom/language-server. Missing peers fail at the relevant feature boundary with the package name and install command; importing the root runtime remains available without them. parse5 remains a normal dependency because the synchronous normalize-tags policy is supported in Workers as well as Node and Lambda.
Documentation
- Getting started: starter creation, project shape, route files, template types, and testing.
- Syntax specification: HTML-first syntax, expressions, directives, components, hydration, and streaming constructs.
- Runtime: signals, ownership, DOM helpers, keyed lists, forms, enhancements, portals, and hydration.
- Public API layers: stability tiers and the root, runtime, generated, server, and tooling import contracts.
- App and Vite: app definitions, file routes, plugins, request-scoped SSR, logging, and packaging.
- Routing: server routes, layouts, loaders, actions, streaming, and client navigation.
- Server adapters: Workers, Node, Lambda, static assets, origins, and deployment behavior.
- Security: escaping, trusted HTML, sanitizers, redirects, CSRF, hosts, proxies, and origins.
- Whitespace migration: compiler versus document policies, legacy mappings, and boundary outcomes.
- Region markers and reactivity migration:
<for>markers,untrack()ownership, memo reads inbatch(),<await>targets, andpending. - SFC scope optimization: setup scope narrowing, emission policies, transform caching, and size comparisons.
- Benchmarks: provenance, contracts, reproduction, and interpretation.
- Releasing: package verification, Trusted Publishing, and release commands.
- Changelog: release history, breaking changes, fixes, and security notes.
Security
Template interpolation and tachyon-dom/server/html escape text by default. rawHtml(), trustedHtmlChunk(), and route stream() chunks are explicit trust boundaries. Sanitize user-generated markup with a vetted runtime adapter, configure public origins and trusted proxy behavior, and apply method plus CSRF/Origin checks before direct form actions.
See the security guide before deploying server-rendered or streamed applications.
Project Status
The current compiler supports text, class, attribute, style, ref, model, and event bindings; keyed lists and conditionals; local stores and components; hydration boundaries; buffered SSR; and streaming targets. The runtime uses fine-grained reactive ownership so component cleanup stops effects, aborts resources, and detaches DOM bindings.
Tachyon DOM is still experimental. Evaluate compatibility, output, accessibility, security boundaries, and benchmark relevance for your application before production adoption.
Development
pnpm install
pnpm test
pnpm build
pnpm lint
pnpm check:exports
pnpm check:size
pnpm check:browser-entry
pnpm check:browser-feature-budgets
pnpm check:runtime-sizes
pnpm check:client-bundle-attribution
pnpm check:template-types
pnpm check:quick-example-size
pnpm check:sfc-scope-sizes
pnpm test:property
pnpm test:mutation
pnpm test:mutation:full
pnpm test:mutation:hydration
pnpm test:mutation:sfc
pnpm test:mutation:store
pnpm test:mutation:release-readinessProperty tests use a stable fast-check seed; use FAST_CHECK_SEED, FAST_CHECK_PATH, and FAST_CHECK_NUM_RUNS to replay or resize a campaign. Mutation testing remains a local check: the normal command reuses incremental results, the full command retests every in-scope mutant, and the detailed report is written to reports/mutation/index.html. The scoped campaigns narrow both the mutated ranges and the tests run against them: the SFC campaign reports to reports/mutation/sfc/, the release-readiness campaign covers the reactive store plus the SFC and client-target ranges a release touches and reports to reports/mutation/release-readiness/, and the store campaign reports to the terminal.
pnpm check:runtime-sizes compares the recorded per-feature runtime measurements and the generated table in the runtime guide. Re-record both with pnpm update:runtime-sizes on a clean checkout after a change that moves them.
The focused hydration campaign reruns the compiler and conditional-runtime ranges from the hydration review without incremental reuse. See its coverage and survivor ledger for scope, remaining equivalent mutations, and GC test requirements.
Useful examples:
pnpm example:web
pnpm example:full-app
pnpm example:streamLicense
MIT
