---
title: Why Markless, technically
description: The technical choices behind Markless on one page, with a mechanism comparison against React, Solid, Svelte, and Qwik.
sidebar: { label: Design choices }
---

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/compiler` runs 15 passes on each module. Before the app runs, it knows each state value, each reader, and each handler. See [the compiler](/how-it-works/the-compiler).
- **One compile renders anywhere.** The same output renders in the browser with `render()`, on a server with `renderToString()`, 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](/how-it-works/the-big-idea).
- **Plain values.** You write `let count = state(0)` and `count++`. `@markless/core` exports `state`, `computed`, `shared`, `element`, and `storage`. 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/router` adds 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-works/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 reports `MARKLESS_EVENT_HANDLER_EMIT_UNSUPPORTED`.
- **Server state must be data.** On the server path, state values travel as JSON records. `@markless/serializer` refuses a function with `MARKLESS_SERIALIZE_UNSUPPORTED_VALUE`.
- **A new file type.** A `.tsrx` file needs the Markless compiler to build. Editors need `@markless/typescript-plugin` to type-check it.

:::warning[Which parts are not stable yet?]
Build-time prerendering runs in the demos behind the `MARKLESS_PRERENDER=1` environment variable. Treat it as a preview. The native targets are proofs of concept. See [native targets](/how-it-works/native-targets).
:::

**Next:** What does the compiler decide, and where can its output run? [The big idea →](/how-it-works/the-big-idea)
