Vue vs Gea
This is a technical comparison for developers evaluating Gea who already know Vue. Both frameworks use proxy-based reactivity, but they differ significantly in their template systems, component models, and rendering strategies.
Architecture
Vue
Vue uses a reactive virtual DOM. Templates (or JSX) are compiled into render functions that produce a virtual DOM tree. When reactive state changes, Vue re-executes the render function, diffs the new virtual tree against the old one, and patches the real DOM. Vue 3's compiler also performs static analysis to hoist static nodes and skip diffing them.
Gea
Gea uses compile-time patching with no virtual DOM. The Vite plugin analyzes your JSX at build time, determines exactly which DOM nodes depend on which state paths, and generates targeted patch functions. When state changes, only those patch functions run — there is no tree diffing, no render function re-execution, and no virtual DOM.
Practical impact: Vue's compiler optimizes the diffing step; Gea eliminates it entirely. For fine-grained updates (a single counter incrementing, a checkbox toggling), Gea touches only the affected DOM node. Vue achieves similar granularity through its compiler hints, but the underlying mechanism is fundamentally different.
Reactivity
Both frameworks use Proxy for reactivity, but the APIs differ significantly.
Vue
import { ref, reactive, computed, watch } from 'vue'
const count = ref(0)
const state = reactive({ todos: [], filter: 'all' })
const filteredTodos = computed(() => {
if (state.filter === 'active') return state.todos.filter(t => !t.done)
return state.todos
})
watch(() => state.filter, (newVal) => {
console.log('Filter changed to', newVal)
})Vue's reactivity API has multiple primitives:
ref()for single values (requires.valueto access)reactive()for objectscomputed()for memoized derived valueswatch()andwatchEffect()for side effectstoRef(),toRefs(),shallowRef(),triggerRef(), etc.
Gea
import { Store } from '@geajs/core'
class TodoStore extends Store {
todos = []
filter = 'all'
get filteredTodos() {
if (this.filter === 'active') return this.todos.filter(t => !t.done)
return this.todos
}
}
export default new TodoStore()Gea's reactivity API is a single pattern:
- Reactive properties per store, wrapped in a deep
Proxy - Mutate directly — no
.value, noreactive()wrapper - Getters for derived values (re-evaluate on access, not memoized)
observe()for side effects (usually generated by the Vite plugin)
Key difference: Vue requires choosing between ref and reactive, remembering .value, and managing the ref unwrapping rules. These are framework-invented concepts with their own rules. Gea's philosophy is that you shouldn't need any of that — just properties on a store that you mutate directly, like any normal JavaScript property assignment.
Templates vs JSX
Vue (Single-File Components)
<template>
<div class="counter">
<span>{{ count }}</span>
<button @click="increment">+</button>
<button @click="decrement">-</button>
</div>
</template>
<script setup>
import { ref } from 'vue'
const count = ref(0)
const increment = () => count.value++
const decrement = () => count.value--
</script>Vue uses its own template syntax with:
for text interpolationv-if,v-else,v-showfor conditionalsv-forfor listsv-modelfor two-way bindingv-bind:(:) for attribute bindingv-on:(@) for event binding
Gea (JSX)
import { Component } from '@geajs/core'
import counterStore from './counter-store'
export default class Counter extends Component {
template() {
return (
<div class="counter">
<span>{counterStore.count}</span>
<button click={counterStore.increment}>+</button>
<button click={counterStore.decrement}>-</button>
</div>
)
}
}Gea uses standard JSX with:
{}for interpolation{cond && <X />}for conditionals.map()for listsvalue+inputfor controlled inputs- Event attributes (
click,input,change— also acceptsonClick,onInput,onChange)
Comparison Table
| Feature | Vue template | Gea JSX |
|---|---|---|
| Text interpolation | | {count} |
| Conditional | v-if="cond" | {cond && <X />} |
| List | v-for="item in items" | {items.map(item => ...)} |
| Two-way binding (inputs) | v-model="text" | value={text} + input={fn} |
| Two-way binding (props) | v-model + defineEmits / defineModel | Direct mutation on shared proxy |
| Event binding | @click="handler" | click={handler} or onClick={handler} |
| Dynamic class | :class="{ active: isActive }" | class={`${isActive ? 'active' : ''}`} |
| Attribute binding | :disabled="flag" | disabled={flag} |
Component Model
Vue
Vue offers three component styles:
Options API:
export default {
data() { return { count: 0 } },
methods: { increment() { this.count++ } },
computed: { doubled() { return this.count * 2 } }
}Composition API:
export default {
setup() {
const count = ref(0)
const increment = () => count.value++
return { count, increment }
}
}<script setup> (recommended):
<script setup>
const count = ref(0)
const increment = () => count.value++
</script>Gea
Gea offers two component styles:
Class component:
export default class Counter extends Component {
template() {
return <div>{counterStore.count}</div>
}
}Function component:
export default function Display({ count }) {
return <div>{count}</div>
}Vue's flexibility means more choices to make (Options vs Composition, ref vs reactive, SFC vs JSX). Gea's philosophy is that JavaScript code should be simple and understandable — it offers two component styles that map directly to language constructs: class for stateful (OOP), function for stateless (functional). There are no framework-specific primitives to choose between.
Side-by-Side: Todo List
Vue
<template>
<div>
<input v-model="draft" @keyup.enter="add" />
<button @click="add">Add</button>
<ul>
<li v-for="todo in todos" :key="todo.id">
<input type="checkbox" :checked="todo.done" @change="toggle(todo.id)" />
<span>{{ todo.text }}</span>
<button @click="remove(todo.id)">x</button>
</li>
</ul>
</div>
</template>
<script setup>
import { ref } from 'vue'
const todos = ref([])
const draft = ref('')
function add() {
if (!draft.value.trim()) return
todos.value.push({ id: Date.now(), text: draft.value, done: false })
draft.value = ''
}
function toggle(id) {
const todo = todos.value.find(t => t.id === id)
if (todo) todo.done = !todo.done
}
function remove(id) {
todos.value = todos.value.filter(t => t.id !== id)
}
</script>Gea
// todo-store.ts
import { Store } from '@geajs/core'
class TodoStore extends Store {
todos = []
draft = ''
add() {
if (!this.draft.trim()) return
this.todos.push({
id: Date.now(), text: this.draft, done: false
})
this.draft = ''
}
toggle(id) {
const todo = this.todos.find(t => t.id === id)
if (todo) todo.done = !todo.done
}
remove(id) {
this.todos = this.todos.filter(t => t.id !== id)
}
}
export default new TodoStore()// app.tsx
import { Component } from '@geajs/core'
import store from './todo-store'
export default class TodoApp extends Component {
template() {
const { todos, draft } = store
return (
<div>
<input
value={draft}
input={e => (store.draft = e.target.value)}
keydown={e => { if (e.key === 'Enter') store.add() }}
/>
<button click={store.add}>Add</button>
<ul>
{todos.map(todo => (
<li key={todo.id}>
<input type="checkbox" checked={todo.done} change={() => store.toggle(todo.id)} />
<span>{todo.text}</span>
<button click={() => store.remove(todo.id)}>x</button>
</li>
))}
</ul>
</div>
)
}
}Both use proxy-based reactivity with direct mutation. The main differences are:
- Vue uses SFC template syntax; Gea uses JSX
- Vue has
v-modelfor two-way binding; Gea usesvalue+input - Vue keeps state in the component; Gea extracts it to a store
Props and Data Flow
Vue
Vue enforces a one-way data flow convention for props. Even though Vue uses proxies internally, it discourages direct mutation of props and provides framework-level abstractions for two-way binding:
<!-- Parent.vue -->
<template>
<Editor v-model:name="user.name" />
</template>
<script setup>
import { reactive } from 'vue'
const user = reactive({ name: 'Alice', age: 30 })
</script><!-- Editor.vue -->
<template>
<input :value="name" @input="$emit('update:name', $event.target.value)" />
</template>
<script setup>
defineProps(['name'])
defineEmits(['update:name'])
</script>Vue's two-way binding requires framework-specific concepts: v-model, defineEmits, defineModel, update:propName events. Each is a framework invention with its own syntax and rules.
Gea
Gea's props follow JavaScript's native value semantics — no framework abstractions needed:
- Primitives are one-way — the child gets a copy (JS pass-by-value).
- Objects and arrays are two-way — the child gets the parent's reactive proxy (JS pass-by-reference). Mutating the object in the child updates the parent's DOM automatically.
// parent.tsx
export default class Parent extends Component {
user = { name: 'Alice', age: 30 }
template() {
return (
<div>
<span>{this.user.name}</span>
<Editor user={this.user} />
</div>
)
}
}// editor.tsx
export default class Editor extends Component {
rename() {
this.props.user.name = 'Bob' // updates Parent's DOM too
}
template({ user }) {
return (
<div>
<span>{user.name}</span>
<button click={this.rename}>Rename</button>
</div>
)
}
}No emit, no v-model, no defineModel. The child mutates the shared proxy directly and every observer updates. This is how JavaScript already works — if you pass an object to a function, the function can mutate it and the caller sees the change.
Comparison Table
| Concern | Vue | Gea |
|---|---|---|
| Primitive props | One-way | One-way (JS pass-by-value) |
| Object/array props | One-way by convention, emit for updates | Two-way (same proxy reference) |
| Two-way binding syntax | v-model + defineEmits / defineModel | Direct mutation — no special syntax |
| Child → parent communication | $emit('update:prop', value) | Direct mutation on shared proxy |
| Deep nesting | Prop drilling or provide/inject | Same object reference at any depth |
Bundle Size
| Stack | Min+Gzip | Includes |
|---|---|---|
| Vue 3.5 | ~33 kb | Rendering only |
| Vue 3.5 + Vue Router 5 + Pinia 3 | ~35 kb | Rendering + state + routing |
| Gea | ~13 kb | Rendering + state + routing |
Vue includes a template compiler, virtual DOM runtime, reactivity system, and component runtime — but not a router or state manager. With Vue Router and Pinia, the total comes to ~35 kb gzipped. Gea moves most work to build time, ships state management and routing out of the box, and still comes in at ~13 kb — 2.7x smaller than a comparable Vue stack.
Directives vs Plain JavaScript
Vue's directive system (v-if, v-for, v-model, v-show, custom directives) is a domain-specific language embedded in HTML templates. It's powerful but adds concepts to learn — a vocabulary that exists only in Vue, not in JavaScript itself.
Gea's philosophy rejects inventing new syntax. Conditionals are && or ternary — standard JavaScript. Lists use .map() — a built-in array method. Two-way binding is value + input — explicit and obvious. There are no directives, no special syntax, and no custom DSL. Every expression in a Gea template is something a JavaScript developer already knows how to read.
Ecosystem
Vue has a rich ecosystem: Vue Router, Pinia, Nuxt, Vuetify, PrimeVue, VueUse, and a large community. The single-file component format has strong IDE support.
Gea provides the core framework, mobile UI primitives, a Vite plugin, a scaffolder, and a VS Code extension. It's a deliberately compact toolkit.
When to Choose Which
Choose Vue when:
- You prefer template syntax over JSX
- You need a mature ecosystem with routing, SSR (Nuxt), and component libraries
- You want
v-modeland built-in two-way binding - You're building a large team project and want abundant resources and training materials
Choose Gea when:
- You believe frameworks should not invent new programming concepts — just leverage the ones JavaScript already has
- You want the smallest possible bundle size
- You prefer JSX and want attribute names close to standard HTML
- You value compile-time optimization that eliminates virtual DOM overhead entirely
- You're building mobile web apps and want built-in view management and gesture support
- You want a simpler reactivity API without
ref/reactive/.valuedistinctions or framework-specific primitives like signals
