Why Markless, technically
The technical choices behind Markless on one page, with a mechanism comparison against React, Solid, Svelte, and Qwik.
Last page asked why Markless insists on rules like a key for every list. The answer is the compiler: it plans everything before your app runs.
The short version
Markless is a compiler for .tsrx components plus a small browser runtime. The repo is at version 0.5.0. The latest npm release is 0.4.0.
- The compiler plans every update.
@markless/compilerruns 15 passes on each module. Before the app runs, it knows each state value, each reader, and each handler. See the compiler. - One compile renders anywhere. The same output renders in the browser with
render(), on a server withrenderToString(), and in tests with@markless/vitest-browser. Build-time prerendering is a preview. A server is one option, not a requirement. See the big idea. - Plain values. You write
let count = state(0)andcount++.@markless/coreexportsstate,computed,shared,element, andstorage. It exports no effect function. - The component body runs once. The body sets up the page one time. After that, only planned DOM writes run. Nothing re-renders.
- Handlers load on first use. The compiler emits each handler as a symbol: a small module that the runtime loads by ID. No symbol loads before its first event.
- Multi-page apps.
@markless/routeradds file routes on Nitro, which deploys to many targets. - No hydration. Hydration means the browser runs components again over server HTML. When a server renders the page, Markless resumes instead. It reads records that the server wrote and continues from them. See the payload.
How it compares
The rows for other projects come from their public docs. I did not run them for this page. The table compares mechanisms, not speed.
| After server HTML | On update | How you write a reactive value | Effect API | |
|---|---|---|---|---|
| React | Hydration | Runs the component again, then diffs a virtual DOM | useState and a setter |
useEffect |
| Solid | Hydration | Signals write the DOM | createSignal, read as count() |
createEffect |
| Svelte 5 | Hydration | Compiled signals | The $state rune |
$effect |
| Qwik | Resumes | Signals | useSignal, read as .value, with $ on lazy code |
useTask$ |
| Markless | Resumes | Compiler-planned DOM writes | state(0), then plain count |
None |
A signal is a value that tells its readers when it changes. Markless keeps a similar graph at runtime. The compiler wires it, so your code never names it.
What it costs
- Handlers have limits. A handler can read state, element handles, props, imports, and values the compiler can write down. If it reads another local, such as a
WeakMap, the compiler reportsMARKLESS_EVENT_HANDLER_EMIT_UNSUPPORTED. - Server state must be data. On the server path, state values travel as JSON records.
@markless/serializerrefuses a function withMARKLESS_SERIALIZE_UNSUPPORTED_VALUE. - A new file type. A
.tsrxfile needs the Markless compiler to build. Editors need@markless/typescript-pluginto type-check it.
Next: What does the compiler decide, and where can its output run? The big idea →
