Gea.js Comprehensive Guide: The Next-Gen Framework Burying the Virtual DOM and Writing 'Just JavaScript'
🇬🇧 Get rid of the Virtual DOM and complex hook structures! Learn how to build high-performance, reactive interfaces by writing pure JavaScript with Gea.js, which is only ~13 KB. Meet its Proxy architecture and build-time optimizations.
In the modern web development world, a new “revolutionary” framework is announced every year. With React, Vue, Svelte, and Solid, as frontend developers, we are constantly forced to learn new syntaxes, new dependency cycles (hooks, dependency arrays), and compiler rules.
But what if I told you there is a way to build reactive and stateful complex interfaces by writing plain JavaScript as you know it, moreover without paying any Virtual DOM (VDOM) and diffing costs?
Meet Gea.js! A brand-new framework developed by Armağan Amcalar (@dashersw) that offers a unique synthesis of “Build-time” JSX transformations and Proxy architecture, featuring a built-in router and state management (batteries-included) at a size of merely ~13 KB.
In this long and detailed guide, we will not only introduce Gea.js superficially; we will dive under the hood to witness line by line how its Vite build plugin works, how Proxy trees are organized in the background, and how it completely leaves the VDOM logic out of the game in your projects.
1. What is Gea.js and Why Do We Need It? 🤔
For many years, the React ecosystem has been trying to hide the slowness of the Virtual DOM with escape hatches that require developer effort, such as memoization (useMemo, useCallback). Although structures like Svelte and Solid turn directly to DOM updates, they distance our code from pure JavaScript with their own compiler “magic” or custom Signal syntaxes.
The core philosophy that Gea.js advocates is this: “A framework should not impose a new programming model on you.” Meaning, in Gea there are:
- No signals.
- No dependency arrays,
useEffect, oruseState. - No templates (template language directives like Vue’s
v-if,v-for). - No complex Redux-like state/reducer architectures.
There are only plain Classes, Objects, Functions, and Getters. While you write your ordinary Vanilla JS code, Gea turns what is written into reactive code using compile-time analysis and ES6 Proxy objects.
2. Under the Hood: The Heart of Gea “Store” and Proxy Architecture 🫀
One of Gea’s unique features is having a built-in State Management solution right in its core architecture. Let’s look together at what the Store class in the @geajs/core package does under the hood:
In the Gea architecture, a Store is just a plain TS/JS Class as we know it, holding the entire state. However, when the Store object is instantiated, Gea wraps this object in a nested (recursive) deep Proxy network.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// A simple example store definition
import { Store } from '@geajs/core'
class TodoStore extends Store {
todos = []
loading = false
// A plain method that mutates state
addTodo(text) {
this.todos.push({ text, done: false })
}
// A Computed Property created strictly using a getter!
get completedTodos() {
return this.todos.filter(t => t.done).length
}
}
export default new TodoStore()
Looking at the code, it seems as if there is no reactivity at all. So what happens in the background?
Proxy Traps and Microtasks
In its source code, the Store class has incredible optimizations for arrays. For example, when you call this.todos.push(...):
- Normally in Vanilla JS, Array methods mutate the object but are hard to track. In the Gea.js source code (via the Store class
_interceptArrayMethodmethod), we see that Array prototypes likepushandspliceare “intercepted”. - The moment you make a change, Gea emits a
StoreChangesignal (type: 'append',type: 'add'). But it does not immediately proceed to a DOM update! - To process events in batches, a
queueMicrotaskis initiated. Even if you update the array or object 50 times in the same synchronous code block, Gea collects these changes (batching) and triggers the DOM update from a single point the moment the call-stack is empty.
Computed Architecture is Just “Getters”
Did you see the get completedTodos() block? The Gea.js Vite plugin, during the build step (store-getter-analysis.ts), finds out which this.todos variable this getter depends on through automatic AST (Abstract Syntax Tree) scanning. Therefore, it understands at compile time that completedTodos will be automatically updated when an addition is made to the list.
Cross-Store Reads and Silent Mutations
Gea does not need massive Redux “Selector” structures. Since Stores are Singletons, a getter in one Store can freely read data from another Store: e.g.: get totalPrice() { return optionsStore.seatPrice + paymentStore.tax }
At the same time, for very intensive UI tasks (drag-and-drop, canvas drawing, etc.), the store.silent(() => { ... }) block saves lives so as not to keep the DOM unnecessarily busy. No data change within this function is broadcasted reactively; while the data is silently manipulated in the background, the DOM is left untouched.
3. Build-Time Magic: Is There Really No Virtual DOM? 🪄
Where Gea is ambitious is its lack of a VDOM. So how does it use JSX and escape the VDOM? The secret lies in the vite-plugin-gea plugin!
The JSX code you write in your project is never sent to the browser as a React-style nested React.createElement(...) structure. Gea’s Vite / Babel integration examines your JSX during the build and converts it into fully optimized native DOM calls (document.createElement, .addEventListener).
Step by Step How it Works:
- The JSX block you write is traversed with Babel Traverse. The plugin separates static HTML structures from variable values (
{store.count}). - The compiled code directly creates a static DOM block in the browser and marks the variable parts in advance.
- When the state changes, Gea does not compare (diff) Virtual DOM trees with each other. It knows exactly where the variable touches the DOM (surgical patch) and instantly changes only the
textNode.textContentvalue right there.
Smart List Updates (applyListChanges)
When you iterate your list with .map() and provide a key prop, Gea runs the famous applyListChanges algorithm in cases of addition (Append), deletion (Delete), or reordering (Reorder/Swap) on the list. Instead of re-rendering the entire HTML list, it only moves the specific DOM node that changes. Unchanged elements do not experience even a millisecond of delay.
Direct DOM References (Goodbye useRef)
There are no useRef and .current assignments, which are famous in React. You can directly write ref={this.canvasEl} and manipulate the native HTML element directly during the component’s onAfterRender() (or onAfterRenderAsync()) lifecycle moment. Accessing the root element directly with this.el reduces code clutter to zero.
Hidden “Web Components” Transformation (Custom Elements)
When you use a component named in “PascalCase” like <TodoItem /> in your JSX code, Gea converts this in the background into standard browser-compatible “kebab-case” native web components (e.g., <todo-item data-prop-...>). If there is a clash with HTML standard tags, it automatically prefixes it as <gea-link>. The framework actually exploits all the native DOM blessings of the Web Components standard without you even noticing, multiplying performance!
Zero “Runtime” Cost in CSS Objects
In React, if you write style=, this object is recalculated and rendered to the browser at every render moment. Gea’s build-time compiler, upon seeing such static style objects, shows immense intelligence and directly converts them into static CSS text (style="background-color: red;") at compile time. Even in style binding, a “Zero Runtime Cost” is targeted.
Time Travel from Function Components to Classes
Pure components that you write as plain functions, containing no lifecycle hooks inside, are taken by Gea during the build step and automatically modified into a class Component extends Store structure (inside the convertFunctionalToClass method)!
So you write in a clean functional style, but the code running in the browser is an OOP class-based object equipped with super-optimized lifecycles.
4. The Collapsing Taboos of Modern Component Architecture 🏗️
What is the thing you pay attention to the most when writing React and Vue? When passing a prop from a parent component to a child, the child component could never change the data inside the prop object (The law of one-way data flow). If you wanted to change it, you had to send an event (emit) or a callback.
In Gea, however, the event has perfect harmony: Props (Objects and Arrays) are Two-Way!
When a Gea Store Object is passed from a parent as a prop to a child, instead of copying the object’s reference value, Gea passes the parent’s reactive watcher (Deep Proxy) itself. If the Child Component changes a value inside this object (like this.props.user.name = 'Ahmet') through normal JS mutation, Gea instantly updates the parent component listening to user.name. There is absolutely no need for you to send a special notification, callback, etc.!
Every Component is Actually a “Store”!
When we examine the base Gea.js library, we see the class Component extends Store definition. Thanks to this architectural marvel, every component you create is already a reactive Store in itself. To manage local state, you do not need to use useState or set up an extra structure; the moment you just write this.isOpen = true inside the class, the component’s local state is directly and reactively updated.
The Missing Practicality of jQuery: this.$() and this.$$()
If you want to do direct manipulation on a specific DOM object inside the component (for example, to set focus on an input object), you do not have to deal with annoying useRef assignments like in React. Keeping developer experience at the top, the Gea class provides you with the built-in methods this.$('input') and this.$$('.item'). Moreover, these selectors perform a pinpoint search not in the global DOM, but entirely only within the component’s own DOM boundaries (root scope)!
Simple Lifecycle Methods
Usually, the most annoying thing when learning a framework is the complex lifecycle system. Gea breaks away from this trouble and offers three or four methods that directly appeal to reason:
created(props): Runs when the component is instantiated in memory for the first time.onAfterRender(): Is triggered asynchronously the exact second the component is physically rendered to the DOM.onAfterRenderAsync(): If you are going to do DOM measurements (getBoundingClientRect) or heavy canvas drawings, this special performance and fluidity-focused lifecycle, which waits for the browser to paint the next frame (requestAnimationFrame), is used so you don’t block the main thread.dispose(): Runs for cleanup purposes when the component completes its life and is removed from the DOM.
Besides all this power, the className nonsense in React becomes a thing of the past completely when writing JSX. You can directly use the Native HTML standard class="..." attribute, and you can provide events using completely standard HTML lowercases (click={this.add}, input={this.change}) instead of onClick.
5. Built-in Router: No Providers, No Hooks, Just a “TypeScript Generics” Show! 🗺️
We said an entire framework is ~13 KB gzipped. This even includes the SPA Router module that resolves all routing!
While you need to install react-router-dom for React and vue-router for Vue and create massive Context Providers, Gea is philosophically extremely lean: The Router is just a standard Store object! You do not need any Provider wrapper or complex hooks like useRouter; to read the state you simply call router.path, and to navigate you just call router.push('/route').
Zero “Codegen”, 100% Type Safety
Synchronizing routing types and parameters is always a headache when working with TypeScript. Gea flawlessly infers all Layout and Parameter types (with InferRouteProps) just by adding as const to the end of your createRouter definition. Moreover, if you want to extract a specific type directly from a string, it is enough to write type Route = ExtractParams<'/users/:userId/posts/:postId'>; the compiler does a powerful TypeScript show by instantly returning the { userId: string; postId: string } object to you. Without the need to install a codegen tool, even parameters in child routes are reflected in your components’ props tree with full support.
Fully Automatic “Lazy-Loading” (Bandwidth Savings) and Error Catching
If the user is never going to visit the “Settings” page, why should they unnecessarily download Settings.js codes in the first place? With Gea, instead of providing the component directly to your route configuration, you just quickly write () => import('./views/Settings'). The system automatically breaks this page down with Code-Splitting and downloads it the moment it’s clicked. If the user’s internet drops exactly at that second or the file fails to download, the application doesn’t crash; Gea instantly and reactively triggers the central router.error state, gracefully paving the way for you to display a warning.
Excellent “Query-Mode” Layout System
It is usually a hassle to set up Tab-based (/settings?view=billing) interfaces in SPAs. The Gea Router, apart from the standard “Path-Mode”, also offers a “Query-Mode Layout”. If you connect a route in “Query” mode, your Layout component is automatically equipped with activeKey, keys, and navigate properties in the background. Without ever refreshing the page, by just triggering the URL parameter, you instantly construct excellent tab interfaces.
Practical and Flexible Guards
The page protection (Guards) mechanism is very simple. A tiny “guard” method you provide to your Routes configuration can instantly redirect if the user hasn’t logged in; or cooler yet, it allows you to manage the process smoothly by inserting instant UI blockers like “Two-Factor Authentication” without changing the URL at all.
“Non-Breaking” and Smart Component
One of the most irritating situations when writing an SPA is the user wanting to Ctrl + Click an internal link to open it in a new tab, but the JavaScript router forcefully opening it in the same page. Gea’s built-in <Link> component is incredibly smart; it perfectly detects modifier keys (ctrl, shift, meta), external links (https://), and right-click events, and never overrides native HTML standard behaviors.
6. A Massively Interconnected Ecosystem: UI and Mobile 📱
The burden of developers crafting UI components from scratch has not gone unnoticed by the Gea team:
@geajs/ui: By fully integrating Zag.js (which uses state machines under the hood), all Headless (pure, where you apply your own design) accessible components like Dropdown, Modal, and Tooltip are provided. So you only draw the CSS boundaries, while Gea handles the logic.@geajs/mobile(Native-Feeling PWAs and Mobile Decks): If your project is a PWA (Progressive Web App), it should give a pure mobile application feel. By just including this package in the project, you gain a massiveViewManagerclass. You no longer advance between pages with plain URLs; you progress as if through a physical deck of cards (this.vm.pull(NewView, true)). When you want to go back, you trigger those famous iOS-style transition animations withthis.vm.push(), swiping and closing the top screen. Inside your PWA deck, modules likeSidebar,TabView,PullToRefresh(Pull down to refresh page), andInfiniteScrollcome ready with a completely native feel. Also, natively assigning built-in gesture supports right inside JSX as an HTML attributetap={fn},swipeRight={fn}is a matchless convenience!
7. Against React and Vue: The “Mental Overhead” War 🧠
When we examine the detailed React and Vue comparison documents in Gea’s repository, we can very clearly see a philosophical rebellion at play:
React’s Mental Overhead: While writing React, you are forced to learn a new “React Language” and sequence of rules rather than JavaScript. Hook rules (inability to call inside an if-block, mandatory ordering), dependency arrays, stale closure bugs, and endless useMemo, useCallback memorizations… All these are actually invented concepts loaded onto the back of the developer to compensate for the render triggers (overhead) in React’s Virtual DOM structure. In Gea, none of these rules exist, because native JS functions and pure classes run.
Vue’s Mental Overhead: Even though Vue cleans things up using Proxies, it offers the developer a massive API surface area. When binding a variable, should you use the Options API or Composition API? Should you choose ref or reactive? If you used ref, your code crashes if you forget to constantly write .value at the end on the JS side. And in the HTML file, you must learn a completely new Vue language (DSL) like v-if, v-for, v-model. In Gea, confusion is zero; state is directly a class property (you say this.counter = 5 and it’s done), and only Vanilla JS features are used within the HTML (&& operator for Conditions, .map() for loops).
In summary, Gea completely frees us from these “Mental overheads” that frameworks force us to memorize!
8. Server-Side Rendering (SSR) and Incredible “Store Isolation” 🌐
While doing SSR (Server-Side Rendering) in React mostly requires massive Next.js configurations, Contexts, and Provider wrappers, Gea’s SSR module (@geajs/ssr) works flawlessly with just 4 main files (server.ts, main.ts, vite.config.ts, index.html)!
Here lie two major features that will literally “Blow your mind”:
- Unmatched Store Isolation: The biggest nightmare when doing SSR is the risk of user “Ahmet’s” information leaking to a user named “Mehmet” (Cross-request state pollution) in globally accessible Store objects. Gea uses Node.js’s
AsyncLocalStoragecapability under the hood to distribute an isolated (“tombstoned”) proxy/data copy to each HTTP request from the exact same global object while you expend zero effort. The leakage risk is 0% and there is no need to write a Context Provider. - Flawless Data Loading (onBeforeRender): Instead of complex
getServerSidePropsspirals in React, a singleonBeforeRender(context)hook runs before SSR rendering. Here you make your API and database requests, populate your Stores, and configure your<head>SEO meta tags; when the HTML is rendered, everything is ready to use! - Deferred Streaming: Is there a slow database request on the page taking 3-4 seconds to load? Gea instantly streams the page’s skeleton (shell) HTML over HTTP and sends it to the browser. Once the database response is resolved (Just like in the logic of React Suspense), it fills that little HTML gap previously reserved on the page with a tiny
<script>chunk. Moreover, it resolves this directly in the SSR configuration without imposing a new component type you have to learn. - Developer Mode (Dev Mode) Hydration Mismatch Protection: If the HTML produced on the server (SSR) and the HTML in your browser (Client) mismatch for some reason (e.g., if you generated a different
Date.now()orMath.random()on the server and the browser), Gea immediately notices this and saves you from a massive mismatch “bug” by logging that 200-character difference to the console (as Hydration mismatch detected). Instead of destroying original DOM nodes top to bottom and re-rendering them back, existing shards are connected by “Adopting” them (sitting on them). - Partial Page Crashes (Fallback HTML with onRenderError): If a tiny component located very deep during SSR cannot find its own data in the database and throws an error, instead of shutting down the entire server (HTTP 500) and kicking all users off the page, it isolates only that tiny collapse using “fallback HTML” via the
onRenderErrormethod. While the remaining 95% of the page loads completely healthy (returns HTTP 200), the troubling spot is politely hidden or filled with an error message.
9. Framework Performance (Benchmark) Evaluation 🚀
In the js-framework-benchmark tests, considered the industry standard of web performance, Gea has captured a massive score with a Weighted Geometric Mean of 1.03 (weighted geometric mean compared to Vanilla JavaScript). This registers not just that it is faster than Virtual DOM libraries, but that it is in the league of, or superior to, rivals like Svelte which touch DOM elements sensitively like a surgeon.
Gea, as a compiled UI library, stands as the framework closest to vanilla JavaScript (handwritten JS) speed. What’s more, without creating a Virtual DOM or a massive runtime weight, it provides you reactive state management, component structure, routing, and JSX architecture entirely integrated (batteries-included).
React, Vue, and Gea Comparison Chart
Also, while you are forced to bring massive structures like state management (Zustand, Pinia) and router (React Router, Vue Router) into the project from the outside in other libraries like React or Vue, in Gea, everything is at the thinnest core of the package:
| Feature | Gea.js | React | Vue |
|---|---|---|---|
| Size (min+gz) | ~13 KB | ~74 KB | ~35 KB |
| Out of the Box | Rendering + State + Routing | + React Router + Zustand | + Vue Router + Pinia |
| Virtual DOM | No | Yes | Yes |
| Reactivity | Proxy-based, automatic | Explicit/Manual (setState, hooks) | Proxy-based (ref/reactive) |
| JSX Classes | class | className | class (In Templates) |
| Event Syntax | click={fn} | onClick={fn} | @click="fn" (In Templates) |
| Props (Object/Array) | Two-Way (Same proxy) | One-Way (Callback up) | One-Way (emit / v-model up) |
If you want to read the philosophical differences at the code level between frameworks, you can access detailed comparisons here: React vs Gea | Vue vs Gea | Full Benchmark Report
10. A World First: Special Skills for Artificial Intelligence (AI) Assistants 🤖
One of the biggest hurdles you face when starting to use a new framework is your AI assistant remaining “clueless” about the topic. Anticipating this, Gea.js comes bundled with its own Agent Skills plugin right within its repo!
When you run this command in your terminal:
1
npx skills add dashersw/gea
.cursor/skills/gea-framework directives are instantly added to your project, training the AI. These plugins ensure that Cursor (and compatible assistants) understand Gea’s Store structure, JSX habits, and reactivity model with full context, and automatically code framework-loyal components for you. Writing code with assistants that make no mistakes is a perfect developer experience!
In addition, the framework hasn’t forgotten developers using Visual Studio Code. Thanks to the official Gea VS Code Extension, you receive 100% Gea-logic “autocomplete” and “Syntax Highlighting” support inside JSX, from its unique class="..." syntax to click={fn} attributes.
11. Philosophical Origin and Real World Examples 🌍
Gea.js is not an idea that formed overnight. It carries the philosophical legacy of erste.js with its legendary simplicity and the state packager regie, developed years ago by its creator Armağan Amcalar: Minimal abstraction and direct DOM ownership. Gea blends these good old principles with the state-of-the-art Vite build-toolchain and Proxy power.
There are also extremely robust practical examples (available in the GitHub Repo) to make your learning easier:
flight-checkin: A great showcase of multi-Stores, E2E (End-to-End) tests, and progressive form navigations.kanban: A complex “drag-and-drop” (drag semantics) board and demo doing heavy state manipulation.mobile-showcase: A mobile showcase displaying the smooth iOS-feeling animations of@geajs/mobilecomponents!
Long Story Short: How Do We Start?
To experience a zero-configuration, ready TypeScript, Vite infrastructure, and Hot Module Replacement (HMR) support with all these structures, go to your terminal and type:
1
2
3
4
npm create gea@latest my-super-project
cd my-super-project
npm install
npm run dev
In conclusion, Gea.js blends the “good old JavaScript” mindset—which makes reading (and writing) code natural, fun, and free from tiresome mental models once again—with “next-gen build-time” performance tools. The true frontend enlightenment, where we get rid of Virtual DOM overhead and maximize the leverage of reactive architecture’s power, might just be starting somewhere around here.
Without wasting any more time, go to the Gea.js Official GitHub Repository to examine the project and its under-the-hood architectural structure yourself, check out the documentation, and don’t forget to star it! 🌟
