# Gea: a JSX compiler that emits HTML strings and patches only what changed

> Gea is a MIT licensed reactive JavaScript framework built on compile-time JSX transforms and deep proxies, with no virtual DOM and no diffing step, published as a monorepo of seven packages. The bundle size comparison in its README is the project's own measurement, and the framework is young enough that the numbers deserve context.

**dashersw/gea** — A batteries-included, reactive JavaScript UI framework. No virtual DOM. Compile-time JSX transforms. Proxy-based stores. Surgical DOM patching.

- Repository: https://github.com/dashersw/gea
- Website: https://geajs.com
- Stars: 1,264 · Forks: 53
- Language: JavaScript
- License: MIT
- Published: 2026-10-19 · Updated: 2026-10-19 · Language: en
- Canonical page: https://hysenlabs.com/projects/dashersw-gea

## What the compiler emits and what the runtime skips

Gea describes itself as a compiler-first reactive JavaScript UI framework, and the four claims in the project description are the whole architecture: no virtual DOM, compile-time JSX transforms, proxy-based stores, surgical DOM patching.

The mechanism is stated plainly. Gea compiles JSX into efficient HTML string templates at build time, tracks state changes through deep proxies, and patches only the DOM nodes that actually depend on the changed data. No diffing, no reconciliation overhead.

That last point is the design decision that everything else follows from. A conventional reactive framework keeps a tree of component state, renders it to a virtual tree, compares the new tree with the old one, and applies the differences to the real DOM. Gea skips the comparison step by deciding at compile time which DOM node depends on which piece of state, so a mutation produces a targeted patch instead of a diff.

The result is a framework where the runtime can disappear from the bundle entirely in the simplest case, which the project describes by borrowing a phrase from Svelte: compile the framework away, taken literally.

The project description uses the word batteries-included, and the package list backs that up. The core package carries stores, components, reactivity and DOM patching, and around it sit a headless UI package, a mobile UI package, a server-side rendering package, a Vite plugin, a scaffolder and an editor extension.

## The bundle size table and what it actually measures

The README leads with numbers, and they are the reason most people will look at this project.

For a fresh Vite production hello-world build, the project reports 121 B of brotli JavaScript for Gea, against 3.6 kB for Solid, 8.5 kB for Svelte, 20.7 kB for Vue and 50.8 kB for React. The table gives the raw minified sizes too, and the versions used: Gea 1.3.0, Solid 1.9.12, Svelte 5.55.5, Vue 3.5.33 and React 19.2.5.

Then it repeats the exercise with a todo application, and this is the more interesting table, because a hello-world can flatter a compiler. The equivalent interactive todo with reactive state, input handling, filtering and item updates ships 4.9 kB of brotli JavaScript for Gea, against 5.7 kB for Solid, 13.7 kB for Svelte, 22.6 kB for Vue and 51.5 kB for React. Including CSS, which was identical across all builds at 2,711 B raw and 746 B brotli, the totals are 5,664 B for Gea and 6,485 B for Solid.

Read those numbers carefully. In the realistic case, Gea and Solid are within 900 bytes of each other, and the gap to the other three is where the real difference sits. The dramatic ratios in the hello-world table, 29.8x against Solid and 420x against React, are an artefact of a case where there is almost nothing to ship.

These are the project's own measurements, taken from fresh Vite 8.0.10 production apps in a working directory named for the comparison, and the README states the method, including that React, Vue and Svelte used equivalent minimal components. That transparency is a point in the project's favour, but a reader should still treat them as one implementation's numbers rather than as a neutral benchmark.

## Stores are classes, reactivity comes from deep proxies

The state model is plain JavaScript with a class wrapper. A store extends Store, holds its state as ordinary fields, and exposes ordinary methods:

```jsx
// counter-store.ts
import { Store } from '@geajs/core'

class CounterStore extends Store {
  count = 0
  increment() { this.count++ }
  decrement() { this.count-- }
}

export default new CounterStore()
```

What makes that work is the proxy layer. The state is tracked through deep proxies, so mutating a nested object is observed at any depth, and the compile-time analysis of the ordinary source code is what wires the reactivity up. The stated principle is that you write this.count++ and the DOM updates, without calling a setter, wrapping values in a signal, or declaring dependencies.

The philosophy section is the clearest statement of intent, and it is a rejection of a specific style of framework design. The project says a framework should not force you to learn a new programming model, and that you should not need signals, dependency arrays, compiler directives, or framework-specific primitives. Stores are classes with state and methods, components are classes with a template() method returning JSX, function components are plain functions with no side effects, and computed values are getters.

The philosophy also says the magic lives only under the hood, in the Vite plugin that analyses the code at compile time. That is the whole bargain of the design, and it is also its main constraint, which the next section covers.

## The Vite plugin is the framework's dependency, not an optional extra

Unlike a runtime framework that works with any build tool, Gea needs its compiler to run. The @geajs/vite-plugin package is described as the Vite plugin handling the JSX transform, the reactivity wiring and hot module replacement, and the examples include runtime-only and runtime-only-jsx directories, which suggests a runtime fallback path exists but is clearly secondary.

The consequence for a project is concrete. The compiler is the part that analyses ordinary JavaScript and works out which DOM node depends on which piece of state. Without that analysis there is no way to know what to patch, and the promise of skipping diffing cannot be kept. So a Gea application is a Vite application, and adopting the framework means adopting the plugin.

The build configuration reflects that. The repository root package.json declares npm workspaces covering packages/* and the website, and every build is a per-workspace script, with build:gea, build:vite-plugin and build:gea-mobile as separate targets. There is a script for the browser and CodeMirror builds of the plugin, which the website playground depends on, and a sync script that copies a playground build of the core into the website. The unit tests run with tsx and the source condition, single-threaded, which is a signal that the test suite depends on module resolution details.

If your build system is not Vite, this is the question to ask before anything else.

## Scaffolding a project and the component model

Getting started is the standard create-and-install flow:

```bash
npm create gea@latest my-app
cd my-app
npm install
npm run dev
```

That scaffolds a Vite powered project with TypeScript, a sample store, class and function components, and hot module replacement.

A component is a class extending Component with a template() method that returns JSX, and it is mounted by calling render on an element:

```jsx
export default class App extends Component {
  template() {
    return (
      <div>
        <h1>{counterStore.count}</h1>
        <button click={counterStore.increment}>+</button>
        <button click={counterStore.decrement}>-</button>
      </div>
    )
  }
}
```

```ts
new App().render(document.getElementById('app'))
```

The event binding syntax is worth noting. The button uses a click attribute bound directly to a method reference on the store, and the interpolation of counterStore.count in the heading is what the compiler tracks. There is no useState, no ref, and no effect hook, which is the concrete shape of the claim that you should not need to learn a new programming model.

The examples directory shows how wide the intended range is, with a chat application, a configurator, a dashboard, a documentation site, an ecommerce store, an email client, a finance app, a flight check-in flow, a forms example, a Jira clone, a kanban board, a music player, a router example, a sheet editor, a showcase and an SSR example. The breadth of examples is a good sign for a young framework, since it means the maintainer exercised the framework against several different application shapes rather than one.

## Seven packages, and only one of them is the core

Gea is a monorepo, and the package list is the best map of what the project covers.

The core package @geajs/core is the framework itself: stores, components, reactivity and DOM patching. The Vite plugin is the compile step. The scaffolder is create-gea, which is what npm create gea@latest calls. The remaining three are capability packages built on the core.

@geajs/ui is headless UI primitives, described as accessible and composable components built on Zag.js. @geajs/mobile is mobile UI primitives covering views, navigation, gestures and layout. @geajs/ssr is server-side rendering with streaming HTML, hydration and store isolation, and store isolation in particular is the piece that matters on a server, where a module-level store shared between two requests would leak one user's state into another's response. There is also gea-tools, a VS Code and Cursor extension providing completions, hover information and diagnostics, which has no npm version listed.

The obvious caveat is that a hello-world application exercises none of the capability packages. The 121 B figure is the core plus the compiler, so it says nothing about what a real interface built on the headless UI layer or the mobile primitives will weigh. Anyone doing a real evaluation should measure their own interface, not the framework's example.

The repository also carries a.changeset/ directory, which is the Changesets tool for managing version bumps and changelogs across a monorepo, and a benchmark/ directory with a benchmark-report.config.json, so the size comparison is a repeatable script in the repository rather than a one-off measurement.

## Maintenance, licensing, and the honest caveat about a young framework

The licence is MIT, with a LICENSE file at the repository root, and the repository is not archived. The last push was on 2026-09-30, and the release history shows the core at 1.5.1 and the Vite plugin at 1.5.0 both on the same day, with 1.5.0 preceding it by an hour. So the project is being worked on right now.

But this is a young framework, and the version numbers tell you that. The core is at 1.5.x, which means the API has been reshaped at least once already, and the size tables in the README were measured against Gea 1.3.0, not against the current 1.5.1. The comparison is therefore two minor versions behind the code in the repository, which is normal for a README that is not regenerated on every release, and it means the numbers should be re-measured rather than trusted as current.

The project is built on the philosophy of two earlier libraries by the same author, erste and regie, carrying forward their core ideas of minimal abstraction, class-based components and direct DOM ownership, and adding the compile-time JSX transforms, deep proxy reactivity and a modern build toolchain. That lineage explains the design: direct DOM ownership is the reason there is no virtual DOM, and it is also the reason the framework assumes it controls the elements it renders.

There is no published stability policy, no compatibility matrix and no long-term support statement in the repository listing. For a framework this new, that is the normal state of affairs rather than a red flag, but it does mean the pinning and upgrade decisions are yours to make, and the changelog in the published packages is the only record of what changed between the version you read about and the version you install.

## Conclusion

Gea is worth evaluating for a small to medium application that values a small bundle and plain JavaScript over a large ecosystem, since the compile-time JSX transform, the proxy based stores and the surgical DOM patching keep both the runtime and the learning curve small; it is the wrong choice if you need a mature component ecosystem, established routing conventions, or a framework with a long track record in production, and before you commit you should check the package list, because the headless UI layer, the mobile primitives and the SSR package are separate from the core and a hello-world page does not exercise any of them.

## FAQ

### How do I create a new Gea project?

Run npm create gea@latest my-app, then cd my-app, npm install and npm run dev. That scaffolds a Vite powered project with TypeScript, a sample store, class and function components, and hot module replacement.

### Does Gea use a virtual DOM?

No. Gea compiles JSX into HTML string templates at build time, tracks state changes through deep proxies, and patches only the DOM nodes that depend on the changed data, so there is no diffing or reconciliation step.

### How does reactivity work in Gea stores?

A store extends Store, keeps its state in ordinary fields and mutates it directly, for example this.count++. The Vite plugin analyses the code at compile time and wires up the reactivity through deep proxies, so no setter, signal or dependency array is needed.

### Which packages does the Gea monorepo publish?

Seven: @geajs/core, @geajs/ui, @geajs/mobile, @geajs/ssr, @geajs/vite-plugin, create-gea and gea-tools, the last being a VS Code and Cursor extension. Only the core and the Vite plugin are needed for a basic application.

### How large is a Gea bundle compared to other frameworks?

The project reports 121 B of brotli JavaScript for a hello-world build and 4.9 kB for an equivalent interactive todo, against 5.7 kB for Solid, 13.7 kB for Svelte, 22.6 kB for Vue and 51.5 kB for React in the todo case. These are the project's own measurements, taken against Gea 1.3.0.

## Sources

- [dashersw/gea on GitHub](https://github.com/dashersw/gea)
- [License: MIT](https://github.com/dashersw/gea/blob/main/LICENSE)
- [Project website](https://geajs.com)
- [README](https://github.com/dashersw/gea/blob/main/README.md)
- [Releases](https://github.com/dashersw/gea/releases)

---

Hysen Labs editorial analysis, written from the project's own repository and release notes. Cite the canonical page: https://hysenlabs.com/projects/dashersw-gea
