初探 Gea:一个编译器优先的 UI 框架
Gea 是一个以编译器为先的 JavaScript UI 框架,使用 JSX、类 store 和直接 DOM 补丁。文章分析其 props 模型、体积和成熟度。
Gea 是一个编译器优先的 JavaScript UI 框架:一个 Vite 插件会在构建期间读取你的 JSX,推断出哪些 DOM 节点依赖哪些状态,然后生成直接的 DOM 补丁。这里没有虚拟 DOM,运行时也不做任何 diff。
宣称能把运行时编译掉的框架每隔几个月就会出现一个,而发布文章通常只给你宣传口号和数字,很少谈取舍。Gea 的页面读起来大体也是这个套路,因此下文将按项目自身的描述梳理其模型,说明每个数字的出处,并重点审视那一件真正与 React 和 Vue 不同的东西:双向对象 props。
核心要点
- Gea 在编译期完成响应式接线:一个 Vite 插件分析 JSX,把状态映射到 DOM 节点,并生成有针对性的补丁,而不是附带一个虚拟 DOM。
- 整个模型只有三条规则:store 是继承
Store的类,组件是带template()方法的类或普通函数,计算值是 getter。 - 对象或数组 prop 传给子组件的是父组件持有的同一个 proxy,因此子组件中的写入也会改变父组件的状态。原始类型则是拷贝传递。
- 项目方报告:hello world 为 121 B(brotli),可交互的 todo 为 4.9 kb,js-framework-benchmark 得分为 1.02——全部由维护者在 Gea 1.3.0 上、针对维护者自己构建的产物测得。
- 只有一位维护者、版本号仍处早期、且没有独立评审,Gea 值得花一个下午做技术验证,而不值得押上生产环境。
Gea 是什么?
Gea 把响应式从运行时挪到了构建期。据项目官网介绍,它的 Vite 插件在编译期间读取你的 JSX,确定哪些 DOM 节点依赖哪些状态片段,并接好只触及这些节点的补丁。与 Vue 的对比文档把差别说得很直白:Vue 会重新执行 render 函数并对生成的树做 diff,而 Gea 只是运行编译器早已生成好的补丁函数。你的状态存放在普通类中,Gea 用深层 proxy 包裹它们,因此像 this.count++ 这样一次普通赋值就足以驱动 DOM 更新。没有东西需要包装,也没有依赖列表需要保持同步。README 把这一脉络呈现为维护者早期的 erste.js 和 regie 库的延续,如今加上了编译期 JSX 转换。
Gea 的模型用三行怎么说清?
Gea 的全部对外面貌都能装进三条规则:store 是继承 Store 的类,组件是带 template() 方法的类或普通函数,计算值是普通 getter。README 里的计数器示例把这些全展示出来了:
// 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>
)
}
}
组件文档说明,Vite 插件在构建时会把函数组件改写为类组件,而组件的 template 只会执行一次。此后发生的一切都是补丁,而非重新渲染。
Gea 的 JSX 与 React 的有何不同?
Gea 的 JSX 看起来和 React 的一样,只有一个约定确实必须改:React 要写 className 的地方,这里写 class。事件则更宽容。README 把它们写成小写属性,如 click、input 和 change,但文档中的 Vue 对比确认 onClick、onInput 和 onChange 也被接受,因此 React 风格的处理函数可以原样迁移。另外 ref 不接受 ref 对象:渲染完成后,Gea 会把 DOM 节点直接挂到组件属性上。
// React habit // Gea equivalent
<div className="card" <div class="card"
onClick={save} /> click={save} />
文档中的 ref 写法是:一个初始化为 null 的类字段,元素上写 ref={this.videoEl},渲染后直接使用 this.videoEl。
Props:对象就是父组件的 proxy
与 React 和 Vue 最大的分歧在于非原始类型 prop 的行为。Gea 的组件文档说明,子组件接收到的正是父组件持有的那个 proxy,因此子组件的任何写入都会落到父组件的状态里,并体现在父组件的 DOM 上。原始类型的行为则和 JavaScript 里的函数参数一样:子组件拿到一份拷贝,对它重新赋值不会影响子组件之外的任何东西。文档给出的示例正是子组件这么做:
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>
}
}
无论组件树有多深,这条规则都成立。把同一个引用交给孙组件,让它写入该对象,所有观察这份数据的祖先组件都会重绘。不需要用回调把状态提升上去,中间也没有 emit 或 v-model 这一步。
| 子组件更新父组件 | React | Vue | Gea |
|---|---|---|---|
| 对象/数组 | 回调 props | emit / v-model | 直接修改共享的 proxy |
| 原始类型 | 回调 props | emit / v-model | 不可能(按值传递) |
文档只把这一点当作优点来呈现,未解的问题则悬而未答。在一棵有 40 个组件的树里,究竟是哪个组件修改了这个对象?React 的单向约定之所以存在,部分原因就是让变更有可追溯的来源;Gea 用它换来了直接性,而这笔交易在规模上的代价,至今还没有人写清楚。编译期接线本身也是同样的情况:编译器遇到无法静态看透的代码时如何处理,文档并未涉及。
体积与速度:项目方给出的数字
关于 Gea 的每一个性能数字,都是项目方自己拿维护者自己构建的竞品框架产物测出来的。README 的体积表格给出的 hello-world 应用为 121 B brotli 压缩后的 JavaScript,对照该项目自行构建的 Solid(3.6 kb)、Svelte(8.5 kb)、Vue(20.7 kb)和 React(50.8 kb),可交互的 todo 为 4.9 kb。两组数据都是在 Gea 1.3.0 上、用全新的 Vite 8.0.10 生产构建取得的。
js-framework-benchmark 得分 1.02(1.00 为手写原生 JavaScript)来自该项目自己在 Chrome 147 上跑的这套测试,而非官方发布的正式轮次。官网首页上”最快的编译型 UI 框架”这句话,是项目方基于同一批自测数字提出的主张,不是第三方结论。
Gea 附带哪些东西?
README 的包列表包括 @geajs/core、@geajs/vite-plugin、用于服务端渲染的 @geajs/ssr、用于脚手架的 create-gea,外加 @geajs/ui(基于 Zag.js 构建的 headless 组件)和提供移动端原语的 @geajs/mobile。gea-tools 是一个 VS Code 与 Cursor 扩展,不是 npm 包。
更不寻常的一项是 AI 工具。运行 npx skills add dashersw/gea 会安装一组 agent skills,它们位于仓库的 .cursor/skills/gea-framework 目录下,把 AI 编码助手原本只能靠猜的约定直接交给它:store 如何工作、组件如何声明、JSX 有何不同。对于一个尚无模型拥有其训练数据的年轻框架来说,把约定以编辑器可消费的 skills 形式交付,是一种务实的上手方式。
成熟度:版本号说明了什么
README 的对比表和体积表都是在 Gea 1.3.0 上测得的,而根据项目的发布说明,@geajs/core 此后已发布 1.4.0。Gea 采用 MIT 许可,由一个人维护,即 Armagan Amcalar。所有基准测试与体积数字都是自报的,目前还不存在对该框架的独立技术评审。对团队而言,结论很直接:这个模型是自洽的,也足够小,一个下午就能评估完,但单一维护者、早期版本号和未经验证的数字意味着,还没有任何证据基础支撑你拿产品去押注它。
Gea 真正有意思的主张不是打包体积,而是:只要有编译器来完成接线,普通的类、函数和 getter 就能承载一套完整的响应式模型。要判断这个主张,最快的办法是把上面的计数器和 props 示例搭出来,看看双向对象语义在你这类代码库里带来的是清晰,还是调试上的隐患。
常见问题
Gea 与 Solid、Svelte 有何不同?
三者都依赖编译器,但响应式原语不同。Solid 的响应式建立在浏览器中运行的 signals 上,Svelte 5 使用由自有模板语言编译而来的 rune 语法。Gea 两者都不用:状态保存在被深层 proxy 包裹的普通类中,由一个 Vite 插件在构建期读取标准 JSX,接好直接的 DOM 补丁。
Gea 组件有哪些生命周期钩子?
Gea 的类组件提供四个钩子。created(props) 在构造函数与首次渲染之间触发,文档建议把初始化逻辑放在这里。onAfterRender() 在组件元素进入文档且其子组件挂载完成后运行。onAfterRenderAsync() 会等到下一个 requestAnimationFrame。dispose() 把组件从 DOM 中移除,并销毁其观察者和子组件。只要你需要用到其中任何一个,文档都会引导你使用类组件。
Gea 支持 TypeScript 吗?
支持。类组件用 'declare props' 声明其 prop 结构,这是一条环境声明,不会产生任何 JavaScript,但能让任何支持 TypeScript 的编辑器对属性进行补全和检查,无需框架插件参与。把 template() 的参数标注为 this['props'],可以把这些类型带入方法内解构出的变量;不标注则会退化为 any。函数组件通过普通的参数标注获得同样的效果。
如何创建一个新的 Gea 项目?
用项目提供的 create-gea 工具执行 'npm create gea@latest' 来生成脚手架。Gea 的构建基于 Vite,而 @geajs/vite-plugin 负责 JSX 转换、响应式接线和热重载。文档还包含一份无需构建步骤、直接在浏览器中使用 Gea 的指南,以及 router、UI kit 和 mobile 包的相关指南。
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