https://github.com/dashersw/erste https://github.com/dashersw/regie
- MIT — Copyright (c) 2017-present Armagan Amcalar: It would be an interesting bout of hubris to give yourself a copyright that predates the beginning of the project by 9 years.
- The README lists sizes as "kb" rather than "KB": I find it odd that it would get units wrong unless it was specifically instructed to do so?
proceeds to introduce Stores and Components
what makes this magically easier than Solid, or any other Proxy-based reactive store frameworks?
I get what you're trying to say, that React hooks have special semantics, and that your abstraction feels more "native".
again, not sure how this is more "native" than Solid signals, just as an example.
That's basically how Gea is more native, because stores are plain classes. I hope this clarifies my point a little bit more.
In the end Gea is basically as simple as a plain old JavaScript class, made reactive by its compiler.
This is a design choice, and explained in their docs:
Separating the read and write capabilities of a store provides a valuable
debugging advantage.
This separation facilitates the tracking and control of the components that
are accessing or changing the values.
> You need to learn its documentation. You always have to use setStore, and it has a weird syntax like `setStore("users", 2, "loggedIn", false)` and even pushing items to an array is weird.It's optional, and incidentally quite expressive and powerful. But they also support mutable drafts out of the box.
import { produce } from 'solid';
setStore(produce(state => {
state.users[2]?.loggedIn = false;
}))Frameworks have APIs; they define concepts. Learning concepts isn't a bad thing in and of itself. Especially if they are concepts which let you model your application more succinctly and efficiently.
What you mean is that you are leaving it to the user to learn (or conceive of) additional concepts which are external to Gea to in order to build non-trivial reactive applications.
But "Gea requires you to write less code / know fewer concepts" can be reframed as "Gea opts out of solving some types of vanilla JS boilerplate for you". When you don't give your users "concepts", they're still going to end up writing a lot of code and learning concepts, just not within your API.
And what kind of types of boilerplate do you see Gea is opting out of?
Immutability and one-way dataflow is an unquestionable productivity win. It eliminates an entire class of complexity, and results in well-defined boundaries for the components of your application. With two-way data binding, those boundaries have to be carefully recognized and preserved by the developer every time they touch the code.
So one place Gea won't save devs any time or grief is in testing. If any part of the app can affect any other part of the app, the surface area of a change very quickly becomes unknowable, and you are only as informed as your tests are thorough. Not boilerplate in the literal sense, but quite a bit of overhead in the form of combinatorial test cases.
Yes, JS has mutability. Yes, you can make two-way data binding work as a framework feature. That you should is an argument I don't think you've successfully made yet.
Let me ask - why do you think JSX lets you model your application more succinctly and efficiently than just a direct createElement call?
The argument for Gea to support two-way binding is basically circular and I believe well-made at this point. I want a framework to respect a language. Breaking two-way binding when it's a concept in the underlying language is like breaking Liskov's Substitution Principle. You can do it, but you probably shouldn't.
JSX is more succinct and efficient than raw DOM API because it's declarative, where the raw API is imperative.
Maybe, but it could be more complicated for you, the maintainer, than it's worth!
> JSX is more succinct and efficient than raw DOM API because it's declarative, where the raw API is imperative.
But that's also the difference between (e.g.) Solid's signals vs a plain (proxied) object that's passed around and mutated. I'd go so far as to say that mutable objects are one of the most "imperative" things about JS.
It's just a native class. There really is no special syntax you need to pay attention to.
Don't confuse syntax with code. Solid has no special syntax (other than JSX of course).This isn't comparing apples to apples.
Solid has a Store primitive too, and it's a "plain old" proxied object.
How is `createStore` less native than `new Store()`? The `new` keyword didn't even exist in JS until 2015, and the underlying semantics (prototypical inheritance) never changed.
One of Solid's design goals is fine-grained reactivity for very high performance. That's why signals are getter functions, because calling them alerts the system to the precise "scope" that will need to be re-run when the value changes.
Since signals are functions, they can be composed easily, and passed around as values.
And yes, Solid has signals that require you to know how to write and work with them. I answered another comment on the thread about Solid stores—they also introduce a couple of gotchas and weird syntax. You just can't do `this.users.push(...)` or `this.users[2].loggedIn = true` in Solid stores.
Therefore `createStore` is less native than `new Store()`, because `new Store()` just gives you a plain (proxied) object you can manipulate in various ways and reactivity will persist thanks to the compiler.
And Gea's design goal is also fine-grained reactivity, which it delivers without getter functions in the code that the developer writes, but rather, the handlers are generated via the compiler.
This part interests me… if it’s able to be brought to React somehow. Too many sites are shipping entirely reactive DOMs where only a tiny minority of content actually changes.
The fact that the entire project appears to have been written in three days, however, gives me some deep doubts.
I've been working on the library for 6 months, and it's built upon my previous libraries tartJS (2011), erste (2017) and regie (2019). I just like to squash my commits before I make a public release, and that just happened 4 days ago :)
And in the end in Gea developers have full control over this, just in the same way they do in real life. `child({ ...obj })` easily solves this, for example, in both idiomatic JS and in Gea.
Why? Why should frameworks be beholden to the mutation semantics of the language, particularly with JS where there is no choice of language in the browser? Why should frameworks follow this paradigm?
In the end, it's a design choice. Of course frameworks don't inherently _need_ to be beholden to the standards of the underlying language, but I think this is just simpler, therefore a worthy goal to pursue.
> But the default expectation (and therefore the design) should follow the practices of the language
Languages do not have practices, developers do.
Regarding "idiom": core language features/semantics are not idioms. In programming, "idiom" usually refers to small commonly-used patterns that reside atop the language. "Mutating objects" is not an idiom, if only because I can think of any number of non-idiomatic uses of mutation.
> If JS allows mutations on the objects passed to a function to be reflected on the parent
JS "allows" mutations on objects to be "reflected" elsewhere, because that's how mutation works. If JS had to support scoped mutability at the language level, the language would be significantly more complex.
But this implies nothing about the value or advisability of using mutation and two-way binding in an application framework. That is a choice on which framework authors usually land on one side or the other.
It seems that by more or less equating "idiom", "practice" and "paradigm", you're opting out of the sorts of choices that not only distinguish web frameworks, but simplify the patterns involved in building with them.
Spreading doesn't prevent you from mutating nested fields. The fact that you think this is an easy problem puts all your other choices under question.
This is a well-trodden path. All aspects of object mutation and its effects are obvious and well-known. What is pass by ref and what is pass by val is also pretty obvious. One can easily pass in primitive values and not worry about two-way binding if they choose to. One can also easily not mutate any props they receive from their parents. This is already the best practice in eslint for like 10 years. This is not easy, this is trivial.
I'd rather see some real concerns.
What I like: the smart compiler that determines the actual dependencies, no need to declare them. Apparently the compiler is so smart as to compute the DOM diffs at compile time, which eliminates the need for virtual DOM.
What kills it for me: the two-way binding. The binding should be one-way to preserve your sanity as the project grows. Two-way bindings allow to build highly reactive Ruby Goldberg machines where anything can trigger anything else, and you won't know, because it's just a mutation of a property somewhere, indistinguishable from a non-reactive mutation. Two-way bindings are callback hell squared.
I want one-way data binding, immutability, and basically FRP. The biggest demonstration of FRP's immense real-life success is not React. It's the spreadsheet.
This may be good for small pieces of interactivity. But I likely would go for HTMX for that.
By the way, just as a syntactic sugar, Gea supports function components, too.
OTOH React arrived where it's now not by allowing a particular approach, but by enforcing it.
Great framework tho. Awesome job.
And thank you!
Of course, one down-side of the compiler approach is, for example, if there's a statement you want to make reactive whose signature is only resolved in runtime (like a computed property name) it's practically impossible to wire. But Gea exposes enough of the underlying component structure so it's kind of straightforward for a developer to manually write an observer for these cases, and do the updates to static properties (that are rendered) in runtime.
Hope this clarifies the approach.
One thing I borrowed from my earlier library erste is event delegation. Instead of creating event handlers bound to each DOM element (say, in a list render) which is memory-heavy and also consumes a lot of CPU cycles, Gea simply attaches one event listener per type on the body and uses a `.matches()` call to check whether that event applies to a given DOM element. This is one of the main reasons why Gea is so performant—there's no excess/unnecessary memory allocation or CPU cycles. This is also reflected in the benchmark results.
One random recommendation I could give based on my experimentations on both firefox and chromium
you can attach symbol based custom properties to each such dom node 'class'
and apart from using .matches(), symbol attribute check. this outperforms class based equality checks by a small margin.
very marginal but hey, you are clearly tryharding.
Vanilla JS, on the other hand, requires a good knowledge of DOM APIs.
Gea tries to be as close to plain old JavaScript as possible, the way we write it on the backend. The only necessary notion is that everything is reactive and DOM will update automatically as component/store members change.
What I find refreshing about Gea is that it doesn't fight the language. Stores are classes. Computed values are getters. State mutation is just assignment. I've been waiting for a framework that embraces the actual paradigms of the language it's written in, rather than inventing a parallel universe of conventions. Excited to try this one.
That said, I'm genuinely curious where the edges are. Was React's complexity accidental due to its architecture or was it the price of solving genuinely hard problems (concurrent rendering, suspense boundaries, fine-grained error recovery) (which by the way most consumers of the library did not care that much about)?
Does Gea's simplicity hold up as apps get complex, or will we eventually hit patterns where the escape hatch is the complexity React already internalized?
I suspect everything about react seems like a backflip to get access to things we kind of needed in the first place.
Reacts complexity I believe are due to the fact that it's an overlay onto a system with high impedance mismatch - only then some degree of inherent complexity.
Gea is frankly very new, and for example doesn't ship a solution for suspenses or fine-grained error recovery yet. And since noone, including me, built a very complex Gea app yet, we don't exactly know if the simplicity will hold up.
But Gea is the 3rd generation of my frontend frameworks, and I've been building vanilla JS-esque frontends since 2011 (when I released my first library, tartJS). The main feature of a good framework is to contain code complexity as the app grows and I believe as GUI engineers we have some good patterns to flatten the complexity. Gea is just trying to hide repetitive DOM updates behind a compiler, and while it has proven somewhat difficult to account for all the different ways people (or AI) write JavaScript, I'm constantly improving the compiler with new patterns.
That's why Gea ships with several GUI app implementations—my approach is kind of simple. If I can get AI to generate several different UI apps in different domains, I can capture a good enough set of complexities for which I can deliver solutions. I've already fixed tens of bugs that I could only see as a result of building a UI with the library.
Having said that, it's still very early for Gea. I guess only time will tell if we will have to resort to different, non-idiomatic solutions to handle more complex demands. At least the philosophy is very clear—Google built its original web 2.0 apps with Google Closure Library, an imperative UI framework, with lots of repetitive boilerplate. And that was enough to give us maps and google docs, etc., so I am hopeful for the future that we will be able to find idiomatic solutions.
Already being used in production at large code bases.
When I use static closures, functions are just functions, closures are closures, things just work. I don't have anything against OOP whatsoever, and I'm even fine with prototype-based OO, but the the way it's done in JS is littered with everything from papercuts to outright landmines. So a framework like WebComponents or this one, that forces me to use JS's OO mechanics, is a non-starter. Of course so are React hooks, which are barkingly insane for completely different reasons.
I'm not overly impressed with the claim "Faster than Solid" when only figure presented on the hero chart is the geometric average of the Duration scores for each framework.
Digging into the individual metrics, Solid is well within the margin of error on essentially every metric to tie or even beat Gea.
On top of that, Solid beats Gea handily on bundle size, 1:4 uncompressed and 1:2 compressed.
So at best, Gea is a tie on speed with Solid, the bundle is bigger, and even time to first paint is a little worse.
Having said that, pure performance wasn't the goal as much as the developer experience. I wanted to build something that felt _native_ to the language.
And thank you for the comment on accessibility—I just updated the website and the docs to make the text more legible.
But all of those metrics differ by something like 1 millisecond, and you've only got benchmark data from an M4 Macbook Pro. On the strength of this, you promise us:
"The fastest compiled UI framework — ahead of Solid, Svelte, Vue, and React."
I know you've put quite a bit of work into the underlying libraries here, but this is the sort of claim people are sure to poke at. Is the Gea code used in the benchmark published anywhere?
<div>window width: {window.innerWidth}</div>
But it's not, it's just on objects that subclass a Store.Of course polling is not fashionable as it's seen as "crude" or "unoptimised" but typically most UI's only have a few dozen input sources at most visible on a screen, and polling that amount of data amounts to less than a ms even on slower mobile hardware. In extreme cases with thousands of data points the compiler could be smart and "short-circuit" the checks, or the component could opt into manual update calls.
But it's super powerful to have what amounts to true immediate mode UI, the entire issue of state management basically goes away. `window.state` becomes a perfectly viable option haha.
Anyways, cool framework.
I get that AI can be good at making websites, and someone might not care to spend a lot of time on it, but a website that looks like a slightly modified version of a generic "make an X landing page" from gpt-5.3-codex doesn't scream "I care about what I just made".
Just go super reductionist like planetscale did and have partially rendered markdown, don't put twinkling stars in the background
First, the claim that one gets "reactivity for free" is not entirely true (although the costs may be negligible for many apps). The stores are wrapped in proxies, which lowers the performance of, for example, property access. This is why I rejected the idea of proxies and instead considered generating getters and setters to handle reactivity. This could, in principle, enable zero overhead reads (assuming that all the reactivity stuff were to be handled in the setter). However, this approach fails to account for, for example, array mutations. Thus, I see the point in proxies, but the cost of them can be significantly lowered performance (which may not matter for all applications).
Second, not memoizing computed values (i.e., getters) can also have a significant negative impact on runtime performance, because expensive computations may have to be executed many times. I suppose that caching could be offloaded to the developer, but this could be laborious, at least if the developer would have to keep track of when to invalidate the computation. In, for example, Excel, computed values can be accessed fast, because they are stored in memory (although the added memory usage can become a problem).
Third, you have not addressed async stores or async computed values (as far as I can tell). I will admit that considering async can be quite complicated. However, for better or worse, async is still a key part of JavaScript (e.g., sync alternatives do not exist for all APIs). Thus, the claims "JavaScript is enough", "Zero Concepts" and "You already know the entire API" are slightly imprecise, because async is not supported (although, this does not necessarily matter for many applications).
These three points were the main reasons why I chose not to pursue my similar ideas (especially in the context of computationally demanding applications). Still, I find the idea that JavaScript should be enough for reactivity to be compelling and worthwhile of further development.
Since Gea doesn't rerender the template _at all (well, for the most part, at least)_, in theory we wouldn't really gain much from getter memoization, mainly because we create proxy observers for computed values that update the DOM in place only when the underlying value updates.
And since stores are plain old JS classes, there's no need for an "async store" concept. Just update a value in the store whenever you want, track its loading state however you want, either synchronously or asynchronously, and the observers will take care of them. If you refer to another pattern that I'm not aware of, please let me know.
Object.defineProperty getter: 82M ops/sec, Proxy getter 32M ops/sec => proxy get is 2.56x slower.
Object.defineProperty setter: 14M ops/sec, Proxy setter: 12M ops/sec => proxy is 1.17x slower.
However, in my simple self written benchmark that compares the time it takes to sum the property values (i.e., getter) of 100 million proxies vs plain objects, the result is that the proxies are 13x slower.
When benchmarking the setting of the property value of the 100 million proxies vs plain objects, the result is that the proxies are 35x slower.
My simple benchmark gives results that significantly deviate from the linked benchmark. Regardless, the relevance of the performance implications of proxies should be evaluated on a case by case basis.
Regarding the memoization, I was primarily referring to accessing the getter multiple times (i.e., not necessarily in DOM rendering), which can cause unnecessary computation in Gea (as far as I can tell). In my envisaged use cases, this could often lead to problems (with, e.g., large data sets).
My issues with async mainly relates to developer convenience (i.e., managing the async stuff can be cumbersome). For example, one can use await in a top-level module statement, but not in a class getter. There has been some relevant discussions about this in the context of Svelte, see, e.g., https://github.com/sveltejs/svelte/discussions/15845, https://www.youtube.com/watch?v=1dATE70wlHc and https://www.youtube.com/watch?v=e-1pVKUWlOQ.
Consider this conceptual example (i.e., async computed value):
store0.url = "...";
store1.res = await fetch(store0.url);
How would this be accomplished in Gea so that reactivity would be preserved (i.e., mutating store0.url would trigger a new fetch)? Is it possible to "listen" to changes to store0.url and handle the async code?Getters in Gea are designed to be used as computed properties by the components' rendering cycles, and in that, they don't need explicit memoization. I believe users can implement their own memoized computed variables if they require repetitive access—in which case each store's `observe` method could be utilized in updating the memoized value whenever a specific value changes in the store.
And for the async case, for now the compiler doesn't handle this scenario. It could be added, but as you expect, for now the same `observe` method could help here as well to listen to changes on store0.url and refetch accordingly.
I find it funny that the headline is "JavaSCript is enough" - yet this is a compiler on top of JavaScript that introduces magic and behavioural changes to syntax. How well does this work with testing frameworks? Can this run without the compiler?
A lot of the thinking behind this compiler comes out of the box with Rust. If only wasm worked.
Gea works best with the compiler, I documented a non-compiled (only compiles JSX) browser usage here: https://geajs.com/docs/browser-usage.html but this obviously requires manual store observers and manual DOM updates, which means it's not _really_ benefiting from Gea.
- would have preferred a syntax like svelte
What syntax would you prefer from Svelte? Like for hooks / stores, or rendering?