12k
All articles

A First Look at Gea, a Compiler-First UI Framework

Gea is a compiler-first JavaScript UI framework using JSX, class stores, and direct DOM patches. The article examines its props model, size claims, and maturity.

OpenReplay Team
OpenReplay Team
A First Look at Gea, a Compiler-First UI Framework

Gea is a compiler-first JavaScript UI framework: a Vite plugin reads your JSX during the build, works out which DOM nodes depend on which state, and emits direct DOM patches. There is no virtual DOM and nothing diffs at runtime.

Frameworks that promise to compile the runtime away turn up every few months, and the launch post usually gives you the pitch and the numbers with little on the trade-offs. Gea’s pages read much the same way, so what follows sets out the model as the project describes it, says where each number comes from, and looks hardest at the one thing that really does differ from React and Vue: two-way object props.

Key Takeaways

  • Gea wires reactivity at compile time: a Vite plugin analyses JSX, maps state to DOM nodes, and emits targeted patches instead of shipping a virtual DOM.
  • The whole model is three rules: stores are classes extending Store, components are classes with a template() method or plain functions, and computed values are getters.
  • An object or array prop hands the child the same proxy the parent holds, so a write in the child moves the parent’s state too. Primitives are copied instead.
  • The project reports 121 B brotli for hello world, 4.9 kb for an interactive todo, and a js-framework-benchmark score of 1.02, all measured at Gea 1.3.0 by the maintainer against the maintainer’s own builds.
  • With one maintainer, an early version number and no independent review, Gea is worth an afternoon spike rather than a production bet.

What Is Gea?

Gea moves reactivity from the runtime to the build. According to the project site, its Vite plugin reads your JSX during compilation, determines which DOM nodes depend on which pieces of state, and wires up patches that touch only those nodes. The Vue comparison puts the contrast bluntly: where Vue re-runs a render function and diffs the resulting tree, Gea simply runs the patch functions the compiler already generated. Your state sits in plain classes that Gea wraps in a deep proxy, so an ordinary assignment like this.count++ is enough to move the DOM. There is nothing to wrap and no dependency list to keep in sync. The README presents this lineage as a continuation of the maintainer’s earlier erste.js and regie libraries, now with compile-time JSX transforms.

What Is Gea’s Model in Three Lines?

Gea’s entire surface fits in three rules: stores are classes extending Store, components are classes with a template() method or plain functions, and computed values are ordinary getters. The README’s counter shows all of it:

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

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

export default new CounterStore()
// app.tsx
import { Component } from '@geajs/core'
import counterStore from './counter-store'

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

The components documentation explains that the Vite plugin rewrites function components into class components while it builds, and that a component’s template runs a single time. Everything after that is a patch rather than a re-render.

How Does Gea’s JSX Differ from React’s?

Gea’s JSX looks like React’s, and one convention genuinely has to change: write class where React wants className. Events are more forgiving. The README writes them as lowercase attributes such as click, input and change, but the docs’ Vue comparison confirms that onClick, onInput and onChange are accepted too, so React-style handlers carry over as they are. And ref takes no ref object: Gea puts the DOM node straight onto the component property once the render is done.

// React habit          // Gea equivalent
<div className="card"   <div class="card"
  onClick={save} />       click={save} />

The docs’ ref pattern is a class field initialised to null, ref={this.videoEl} on the element, then direct use of this.videoEl after render.

Props: Objects Are the Parent’s Proxy

The biggest departure from React and Vue is what happens to a non-primitive prop. Gea’s components documentation explains that the child receives the very same proxy the parent holds, so any write the child makes lands in the parent’s state and shows up in the parent’s DOM. Primitives behave the way function arguments behave in JavaScript: the child gets a copy, and reassigning it changes nothing outside the child. The docs show a child doing exactly this:

export default class Editor extends Component {
  rename() {
    this.props.user.name = 'Bob'   // parent's DOM updates too
  }
  template({ user }) {
    return <button click={this.rename}>{user.name}</button>
  }
}

The rule holds however deep the tree goes. Hand the same reference to a grandchild, let it write to the object, and every ancestor watching that data redraws. Nothing has to be lifted with a callback, and there is no emit or v-model step in between.

Child updates parentReactVueGea
Objects/arraysCallback propsemit / v-modelDirect mutation of the shared proxy
PrimitivesCallback propsemit / v-modelNot possible (pass-by-value)

The docs present this purely as a benefit, and the open questions sit unanswered. In a 40-component tree, which component mutated this object? React’s one-way convention exists partly so that changes have a traceable origin; Gea trades that for directness, and nobody has yet written up what that trade costs at scale. The same goes for compile-time wiring itself: how the compiler handles code it cannot statically see through is not something the documentation addresses.

Size and Speed, as the Project Reports Them

Every performance number about Gea is the project’s own measurement against the maintainer’s own builds of competing frameworks. The README’s size tables put a hello-world app at 121 B of brotli JavaScript, next to the project’s builds of Solid (3.6 kb), Svelte (8.5 kb), Vue (20.7 kb) and React (50.8 kb), with the interactive todo at 4.9 kb. Both sets of figures were taken at Gea 1.3.0 from fresh Vite 8.0.10 production builds.

The js-framework-benchmark score of 1.02, where 1.00 is hand-written vanilla JavaScript, comes from the project’s own run of the suite on Chrome 147, not from an official published round. The “fastest compiled UI framework” line on the homepage is the project’s claim built on those same self-run numbers, not a third-party finding.

What Ships Alongside Gea?

The README’s package table lists @geajs/core, @geajs/vite-plugin, @geajs/ssr for server-side rendering, create-gea for scaffolding, plus @geajs/ui (headless components built on Zag.js) and @geajs/mobile for mobile primitives. gea-tools is a VS Code and Cursor extension, not an npm package.

The more unusual entry is AI tooling. Running npx skills add dashersw/gea installs a set of agent skills that live in the repo under .cursor/skills/gea-framework and hand an AI coding assistant the conventions it would otherwise have to guess at: how stores work, how components are declared, and how the JSX differs. For a young framework that no model has training data on, shipping the conventions as editor-consumable skills is a pragmatic onboarding move.

Maturity: What the Version Numbers Tell You

The README’s comparison and size tables are measured at Gea 1.3.0, and @geajs/core has since published 1.4.0 according to the project’s release notes. Gea is MIT-licensed, and it is maintained by one person, Armagan Amcalar. Every benchmark and size figure is self-reported, and no independent technical review of the framework exists yet. For a team, the conclusion is straightforward: the model is coherent and small enough to evaluate in an afternoon, but a single maintainer, an early version number, and unverified numbers mean there is no evidence base yet for betting a product on it.

Gea’s genuinely interesting claim is not the bundle size; it is that plain classes, functions, and getters can carry a full reactivity model if a compiler does the wiring. The fastest way to judge that claim is to scaffold the counter and the props example above and see whether the two-way object semantics feel like clarity or like a debugging liability in your kind of codebase.

FAQs

How is Gea different from Solid and Svelte?

All three lean on a compiler, but the reactive primitive differs. Solid builds reactivity on signals that run in the browser, and Svelte 5 uses its rune syntax compiled from its own template language. Gea has neither: state is held in plain classes that a deep proxy wraps, and a Vite plugin reads standard JSX at build time to wire direct DOM patches.

What lifecycle hooks do Gea components have?

Gea class components expose four hooks. created(props) fires between the constructor and the first render, and is where the docs put initialisation logic. onAfterRender() runs once the component's element is in the document and its children have mounted. onAfterRenderAsync() waits for the next requestAnimationFrame. dispose() pulls the component back out of the DOM and tears down its observers and children. The docs point you at class components whenever you need any of this.

Does Gea work with TypeScript?

Yes. A class component states its prop shape with 'declare props', an ambient declaration that emits no JavaScript but lets any TypeScript-aware editor complete and check the attributes, with no framework plugin involved. Annotating the template() parameter as this['props'] carries those types into the destructured variables inside the method; skip it and they fall back to any. Function components get the same treatment from a normal parameter annotation.

How do I start a new Gea project?

Scaffold one with 'npm create gea@latest', the project's create-gea tool. Gea builds sit on Vite, and @geajs/vite-plugin is what handles the JSX transform, the reactivity wiring and hot reloading. The documentation also includes a browser usage guide for running Gea without a build step, alongside guides for the router, UI kit, and mobile packages.

DevTools for the frontend

Gain Debugging Superpowers

Unleash the power of session replay to reproduce bugs, track slowdowns and uncover frustrations in your app. Get complete visibility into your frontend with OpenReplay — the most advanced open-source session replay tool for developers.

Star on GitHub12k

We use cookies to improve your experience. By using our site, you accept cookies.