NeoComp: LightWeight UI with a Unique Twist
It was well known that before there was a new AI model every week, there was a new JS framework every week.
The Modern JS Revolution
Section titled “The Modern JS Revolution”It is impossible not to notice the massive JS revolution that took place over the past decade or two. When the web was still viewed as a clunky document viewer powered by prehistoric, taped-together tech, in the blink of an eye, copying the web became the norm, and JS skyrocketed into the stratosphere.
While web engines were being engineered into masterpieces, the frontend world was tackling a higher-level concept: making UI definition more ergonomic, and they mastered it.
While the rest of the world was building interfaces the 90s way, the web world was pioneering the frontiers of UI innovation. UI became a clean mathematical formula declared in concise syntax with reactivity as its core foundation. Supported by unmatched tooling, strict type-safety, and highly intelligent IDEs, frictions vanished, and developers flowed with their UI.
It was a golden age, and what it produced was even more remarkable: a quick trip back to the Stone Age with every page navigation, a sudden stutter on every click to remind you to upgrade your high-end workstation, 10 megabytes worth of JS just for a simple counter, and a CI pipeline that built an entire compiler toolchain as an artifact.
Did we sell the lightweight and simple soul of JavaScript for the sake of removing two additional characters? Actually, yes.
This is why I present NeoComp, a conceptual framework built to prove a point: for God’s sake, dear frontend developers, the only remaining frontier is Descriptive UI—describing UI in plain English and letting AI generate the code on every route. If we just bring back those two characters, we can have it all: ergonomic, declarative reactivity combined with the old-school, lightweight nature of JavaScript in a unique way.
With that introduction out of the way, let’s dive into how we got here.
The Old Web
Section titled “The Old Web”Every sane person knows that JS was doomed from the start. Yet, it not only survived, it thrived. Why? Because it works. It is more than simple, capable, and lightweight for a scripting language originally designed to add a quick touch of reactivity to a small, static site.
This humble persona was clearly reflected in the old-school UI boilerplates. The first UI libraries weren’t designed for complex, highly-architected applications, they were a few liners built for pure utility and minimal verbosity. Think jQuery, Prototype.js, MooTools, and the like.
<div id="counter"> <button class="dec">dec</button> <span class="count-value">0</span> <button class="inc">inc</button></div>
<script> // the old days $(document).ready(function () { var count = 0; var $countSpan = $('.count-value');
$('.dec').click(function () { count--; console.log('count: ', count); $countSpan.text(count); });
$('.inc').click(function () { count++; console.log('count: ', count); $countSpan.text(count); }); });</script>However, as time went on and ambitions grew more and more, manually targeting elements and attaching listeners and updaters were quickly thrown out the window for the trendy /MV.+/ family to arrive: MVP, MVC, MVVM, MVT, HMVC, PAC, MVA, VIPER, MVVM-C, MV-Whatever.
<div id="counter"></div><script> // Ah, this is a simple counter, right? const counterModel = new Backbone.Model({ count: 0 }); const CounterView = Backbone.View.extend({ el: '#counter', events: { 'click .dec': function () { counterModel.set('count', counterModel.get('count') - 1); }, 'click .inc': function () { counterModel.set('count', counterModel.get('count') + 1); }, }, initialize: function () { this.$el.html(` <button class="dec">dec</button> <span>${this.model.get('count')}</span> <button class="inc">inc</button> `);
this.$count = this.$('.count-value'); this.listenTo(counterModel, 'change:count', this.updateCount); }, updateCount: function (model, count) { console.log('count: ', count); this.$count.text(count); }, }); new CounterView();</script>This explosion of paradigms took over the web and made JavaScript less “scripty” and more “Javay.” It forced OOP design patterns onto a language that didn’t even have proper syntax for inheritance. It demanded a strict separation of concerns between the Model and the View, pushing developers to fight holy wars over which ideology best represented that goal, all while jQuery quietly continued to power the world.
While this era was essential for the web’s evolution, elevating JavaScript from scribbles of scripts into real, architected applications, it focused entirely on structure and design and ignored the real root of the issue. And out of that chaos, React was born to declare the web truly reactive.
Why React Won
Section titled “Why React Won”It is difficult to comprehend how React won this hard, how it turned a community full of OOP traditionalists into functional purists. They say it was the component model, others say it was JSX, and some say it was the VDOM. However, if you actually look deep into it, you will find the real cause was declarative reactivity to infinity and beyond.
Yes, we did have reactivity and magic, but it was just simple, humble utility. The reactivity that React pioneered belongs to another world, one that even most modern frameworks can’t replicate with such simple syntax. The bindings are not reactive, the states are not either, the UI itself is the reactivity primitive.
React transformed components from naive manual binders into mathematically pure UI functions. You don’t construct UI, you compute it. You transform the state into UI blocks, use native control flow to select and join them together, and with the power of components, you step up the abstraction levels and compose the UI in your own declarative way. You are free to flow while React handles the rest.
export default function Counter() { const [count, setCount] = useState(0);
useEffect(() => { console.log('count: ', count); }, [count]);
return ( <div id="counter"> <button onClick={() => setCount(count - 1)}>dec</button> <span>{count}</span> <button onClick={() => setCount(count + 1)}>inc</button> </div> );}That reactivity paradigm was the real spark that began and powered the modern JS revolution, and with it, the recent web copying phenomenon. It was a miracle that we served with our best ideas and most sophisticated tools, along with our engineering workshops and optimization journeys.
However, it cost us everything.
The Cost of React Reactivity
Section titled “The Cost of React Reactivity”React’s best selling point was also its death sentence. The React component model requires its code to rerun on every single update. This is the only practical way to handle coupled reactivity code, but it is also the most beautiful existential curse you can gift to performance.
While React components are meant to be lightweight, pure, and updated locally, the truth is that React code is the application code. The components are packed with side effects, and the global scope is connected to even the tiniest component in indirect ways.
In most React applications you encounter in the wild, any little click reruns megabytes of heavy application logic every single time, leading to serious stutters even on heavy workstations. And no, the misery doesn’t stop there. Due to modern requirements, aggressive sources of updates have invaded most apps, from complex animations to streaming content, 60-FPSing the stutters you love.
A native developer would thank God they don’t have to interact that much with the web. But I would like to inform you that, because of business appeals, almost every heavy native application is now being written in React.
This, my dear friend, is called over-rendering hell,the true source of the sluggishness of the modern web. Actually, it should be called over-updating hell, since the DOM is being updated and rendered efficiently by the browser’s rendering engine. The application logic itself is the bottleneck, and that is how React got away with this.
The VDOM Myth
Section titled “The VDOM Myth”React initially responded to this criticism by explaining that updating the DOM is the slowest part of the process, and that React uses a highly optimized algorithm called reconciliation, where two lightweight representations of the UI are diffed together, and only the necessary changes are made. This, they claimed, made React faster than manual code.
First, yes: the JS engine is an engineering beast that is highly optimized for creating and managing millions of tiny objects. Yes, the DOM is slow if you trigger layout reflows. And yes, the React runtime is heavily over-engineered for performance.
But second… what was the “manual code” that React was actually comparing itself to?
document.body.innerHTML = app(state);Now the image is a lot clearer.
The Aftermath of React Reactivity
Section titled “The Aftermath of React Reactivity”React was our revolution against traditional OOP verbosity and over-abstraction, carrying us into the world of functional purity and declarative reactivity. However, we dove so deep into declarative reactivity that we became restrained by its own computational complexity.
It is exactly how we escaped the chaos of the early scripting era by fleeing to the structured architectures of MVC, only to fall too deeply into OOP hyper-perfection and over-categorization.
This is how evolution occurs and develops. We must push React in a better direction by rethinking reactivity. And yeah, React has indeed become the new jQuery. It is now the default option, and consequently, the most restrained one. Rethinking React requires more than just internal refactoring, it requires a complete flip of our ideology and philosophy.
A new React can easily coexist with the old React, but the mental frameworks we developed over the past decade will have to be garbage-collected. Even though the React team has done a great job enhancing React and not breaking the web, Like React Fiber and React Compiler, they are just internal optimizations and not overall rethinking. Yet, the shift is theoretically viable, economically impossible, but youthfully ongoing.
There have been many attempts to fix React and push the web forward over the past decade. Some tried to patch React with minor improvements, while others were revolutionary, packed with fresh ideas and energy. They dove deep into the depths of innovation and came up with brilliant concepts. The most successful and important of them all is SolidJS.
Solid is a frontend framework developed by Ryan Carniato that focused on one thing: merging a React-style declarative UI with fine-grained reactivity, while keeping build steps to an absolute minimum. It successfully fulfilled its goal and pushed fine-grained reactivity back to the forefront of web development.
Fine-Grained Reactivity: The Best Reactivity
Section titled “Fine-Grained Reactivity: The Best Reactivity”Fine-grained reactivity is reactivity that is efficient and performant. It only runs the exact, required chunk of code on each update, unlike coarse reactivity, which runs more than needed, and sometimes, the entire application.
While it may seem like a modern invention, fine-grained reactivity was invented before the web even existed. The most primitive form of binding you created back in the scripting era is, in fact, fine-grained reactivity. Most reactive solutions that relied on observables are fine-grained if they set up their dispatching correctly. In addition, compilers like Svelte emit raw, fine-grained reactive code under the hood.
Solid didn’t invent fine-grained reactivity, nor was it the first to push it to the web. What it did was much more brilliant: it incorporated it into a React-style declarative UI.
React-Style UI, the Solid Way
Section titled “React-Style UI, the Solid Way”Solid kept the declarative UI patterns we all adore. It maintained functional components, JSX template syntax, hooks, and a very similar overall developer feel.
However, it flipped components from mathematical UI functions into builder functions. In Solid, a component runs exactly once to initialize the UI structure and its initial state, it also declares the bindings. After that, the component steps aside, passing full responsibility to the fine-grained reactivity engine, which dispatches updates in a highly efficient, targeted flow.
export default function Counter() { const [count, setCount] = createSignal(0);
createEffect(() => console.log('count: ', count()));
return ( <div id="counter"> <button onClick={() => setCount(count() - 1)}>dec</button> <span>{count()}</span> <button onClick={() => setCount(count() + 1)}>inc</button> </div> );}If you look at Solid code, you won’t see much difference compared to the corresponding React code, mostly just naming changes. However, if you squeeze your eyes a little bit, you will notice some extra parentheses for some reason. That is exactly how Solid achieves its fine-grained magic.
Improved Reactivity Primitives
Section titled “Improved Reactivity Primitives”The primary and most crucial thing Solid transformed was reactive code separation. It took the massive, unorganized blob of reactive code and extracted it from the static code, slicing it into smaller, focused chunks with clear boundaries called effects.
Effects are essentially why fine-grained reactivity is achievable. By isolating side effects, only the required snippets of reactivity are executed on an update. It freed us from the chaotic, disordered nature of React reactivity, where we were forced to rerun the entire application logic on every state change.
Moreover, effects are not limited to pure logic code, they are the engine of simple bindings. When defining bindings inside JSX, you encapsulate the computed expression inside a closure, which the template engine in turn transforms into an isolated effect.
In the spirit of improvement, Solid wrapped state in lightweight boxes called signals. Reactive state cannot be acted on like plain data, and unlike frameworks that rely on heavy compilers and complex infrastructure to fake a rigidly controlled, “semi-vanilla” vibe, Solid took the honest, vanilla route of raw signals.
This route unlocked several essential capabilities, ranging from passing reactivity with data, accessing the latest versions of state, triggering updates, and most importantly, tracking dependencies implicitly.
The Brilliance of Implicit Minimal Syntax
Section titled “The Brilliance of Implicit Minimal Syntax”Each effect holds a specific list of dependencies. However, manually declaring this list is verbose and highly prone to errors. Solid doesn’t require manual declaration because it simply infers them. Magic? No, just engineering brilliance.
Remember that signals know exactly when they are accessed. We can use this information to infer the dependencies during the effect’s initial execution. This isn’t magic or complex compiler analysis, it is just a clever, lightweight runtime trick.
This single, life-saving detail summarizes Solid’s philosophy and mission. It identified React’s core problem, an over-pushed reactivity model that forced an ultra-heavy runtime and execution paradigm, and presented a solution showing how, by slightly restraining it, we maintain that elite level of declarativity while achieving near-metal performance.
Solid didn’t enforce heavy restrictions, nor did it insanely increase the syntax load. It just added, or rather restored, a few basic characters: state(), state(value), () => state() * 2, and effect(() => console.log(state())). Not only did a few parentheses here and there solve our existential performance threat, but they also eliminated a ton of restrictions on us. We can even use signals across document boundaries.
export default function CrossCounter() { let [count, setCount] = createSignal(0);
// This isn't crazy... relatively to JavaScript. let iframe = document.createElement('iframe'); let button = document.createElement('button'); button.onclick = () => setCount(count() + 1); button.textContent = 'increment'; setTimeout(() => iframe.contentDocument?.body.append(button));
return ( <div> count: {count()} {iframe} </div> );}The Reality of Fine-Grained Reactivity
Section titled “The Reality of Fine-Grained Reactivity”It is true that React’s declarative power is from another planet, theoretically. In practice, it’s negligible. React can handle any theoretical UI structure and render it efficiently with zero syntax overhead. But the actual, physical nature of the web is static structure, dynamic content.
Every HTML page on the web has a fixed structure. What is dynamic is the simple bindings and the occasional templated structure that gets added or removed, mostly hidden or shown using the power of CSS. And that is precisely where fine-grained reactivity shines. The only high-density structural changes are page transitions, and those belong to another document and another context entirely. Solid optimizes for real-world usage, not theoretical ideals.
Solid didn’t invent fine-grained reactivity. Inspired by Knockout, its calling was to bring fine-grained reactivity back to the mainstream, and it succeeded. Almost every modern JS framework has incorporated signals and fine-grained reactivity into its stack, even if only at the component level. Except React.
And if you look at the framework benchmarks, you will be glad to see most of the scores hovering right near the vanilla baseline, and sometimes, even beating it. Fine-grained reactivity really won.
You might assume that potential, simple-yet-revolutionary innovations in UI frameworks plateaued after SolidJS. The reality is that we were just sitting on a local plateau. Soon, a new framework would come suddenly, creating ripples in the web world. It targeted not the already-mastered reactivity, but its closest friend: templating, and its very identity.
Ripple
Section titled “Ripple”Ripple.ts is a frontend framework developed by Dominic Gannaway that emerged in September 2025. It arrived and shocked the web world with a concept that targeted the very heart of JSX, a concept that promised to skyrocket the expressiveness of our templates and eliminate their decade-old restrictions.
It was born to provide a highly capable solution to the decade-old problem.
JSX is Kinda Limiting
Section titled “JSX is Kinda Limiting”Before discussing the limitations of JSX, let’s dive into some historical trivia.
In simple terms, JSX is JavaScript + HTML expressions. Formally, it utilizes angle-bracket delimiters in primary expressions to introduce HTML syntax to ECMAScript, with minor additions like the {} embedded expression syntax.
Writing HTML inside JavaScript was pushed to the mainstream by Facebook with React, but its roots are buried deep in web history. Other than the template strings and the recent tagged html literals, JavaScript actually supported HTML comments (<!-- comment -->). Even ECMAScript 4 tried to introduce native XML syntax to the language through E4X (ECMAScript for XML), the true precursor to JSX (JavaScript XML).
default xml namespace = "http://www.w3.org/1999/xhtml";
let active = true;var database = <users> <user id="101" role="guest"> <name>Alice</name> <status>inactive</status> </user> <user id="102" role="user"> <name>Bob</name> <status>{active}</status> <tags> <tag>newbie</tag> </tags> </user></users>;
// XPath running natively in JSprint(database..user.(name == "Bob").tags.tag);database..user.(@id == "101").@role = 'admin';database..user.(name == "Alice") += <profile><bio>Joined today!</bio></profile>;database..user.(@id == "101").* = "Access Revoked";The primary limitation of JSX stems from the fact that it returns a value, and its embedded expressions must be expressions. While JS expressions are powerful, even allowing you to declare classes inline, JavaScript, like the majority of C-style languages, is statement-oriented, not expression-oriented.
Consequently, its expression-based control flow is incredibly lacking. It cannot go beyond a single level of ternary depth, and it completely lacks looping or jumping expressions.
The awkward moments we all experience in JSX when trying to implement anything more complicated than a simple binding are symptoms of this design. The lack of control-flow expressions leads to the extreme abuse of the ternary operator, logical operators, and Array.map, resulting in the indentation hell pre-bundled with every slightly complex JSX component.
// In some places, if you pass the 3-indent depth mark, you are killed.// Here the indent itself triggers word wrap and life is good.export function JSXHell({ items }) { return ( <div> {items.length ? items.map((item) => ( <div> <h1>{item.title}</h1> {item.description && <p>{item.description}</p>} </div> )) : 'No items found.'} </div> );}
// The only solution: OOP-inspired over-abstraction.function JSXHellItem({ title, description }) { return ( <div> <h1>{title}</h1> {description && <p>{description}</p>} </div> );}export function JSXHell2({ items }) { return ( <div> {items.length ? items.map((item) => <JSXHellItem {...item} />) : 'No items found.'} </div> );}Statements in Templates
Section titled “Statements in Templates”If we don’t have good control-flow expressions, why not just use statements? While it sounds bizarre at first, it is actually possible, syntactically beautiful, potentially powerful, and serves as the headline selling feature of Ripple: the statement container @{}.
Conceptually, the statement container is a block expression that can be placed as a JSX child. It is a block composed of multiple statements of any kind, from expressions to control flow to even declarations. It can host its own local variables, and it must end with a JSX expression.
If you visualize how this works, a smile will likely appear on your face. The restrictive limitation of only component-scope logic vanishes. At any point in your template, you can open a block and write that specific section’s state and helper logic directly inline with its structure. You can nest this infinitely deep, the only limitation is your own indentation redline.
export function Example() { return <div> <h1>counter</h1> @{ let &[count] = track(0); effect(() => console.log('count: ', count)); <> <button onClick={() => count--}>dec</button> <span>{count}</span> <button onClick={() => count++}>inc</button> </> } <h1>inputter</h1> @{ let &[text] = track(''); <> <input onInput={(e) => text = e.target.value} /> <span>{text}</span> </> } </div>;}Additionally, you can use native control flow statements prefixed with @. They work exactly as expected, except they are reactive, with their blocks acting as statement containers.
export default function App() @{ let list = new RippleArray();
let i = 0; setInterval(() => list.push(i++), 1000)
<div> @for (let item of list) { <> <div>{item}</div> @if (item % 3 == 2) { <hr/> } </> } </div>}This is one of the biggest revolutions in the history of JSX and templating as a whole. It unlocked a massive wave of architectural potential, enabled flexible patterns, and made nesting components effortless. Yet, like most inventions, it has a slight but noticeable catch.
Non-Standard Syntax
Section titled “Non-Standard Syntax”The JS developer community loves adding new syntax to the language. While extending the language adds a ton of usefulness and expressiveness, it has the major drawback of being non-standard. This breaks everything consuming the language: the runtimes, formatters, linters, editors, and the broader tooling ecosystem.
While these issues can be resolved by a custom build step, and our tools can be configured to support those extensions, with some even becoming semi-standardized, like TypeScript and JSX, they still remain non-vanilla.
I respect the designers of these extensions from the bottom of my heart. TypeScript, for example, is a core feature that should have been native to JavaScript two and a half decades ago. However, non-standard extensions kill the basic promise of JavaScript: the ability to run everywhere. They also prevent us from benefiting from the collective engineering of the platform layer, the browser and engine maintainers.
Fortunately, some of these non-standard complications can be eliminated entirely… using only three extra characters.
NeoComp
Section titled “NeoComp”Now we have arrived at the main dish of this article. NeoComp is similar to every lightweight and vanilla modern framework. Inspired by SolidJS, it is powered by fine-grained reactivity and uses tagged html literals for templating.
Tagged Literals
Section titled “Tagged Literals”Tagged literals are normal functions called using the template literal syntax. They are the closest thing we have to native macros. While they can’t introduce new global syntax, they can express any block syntax that is compatible with template literals, and HTML is from that family.
Some frameworks like Lit and Solid use this feature to create a full vanilla experience. Contradictory to exceptions, they support full tooling: highlighting, formatting, and complete IntelliSense, and are even simpler to implement.
A full JSX experience can be achieved all thanks to 3 characters: the ` ` that delimit the HTML structure, and the $ that prefixes the embedded expressions. Yet, we love magic, even if it haunts our soul.
function Vanilla () { let count = signal(0); return html`<div> <button on:click=${() => count.value--}>dec</button> <span>${count}</span> <button on:click=${() => count.value++}>inc</button> </div>`;}This would be a pretty great stack for a lightweight vanilla framework, however, with the arrival of Ripple, template expressions became old-fashioned, and statements in templates became the cool guy. So, NeoComp came up with a unique and innovative twist from the depths of imperative UI.
Builder Pattern
Section titled “Builder Pattern”The existential question that inevitably comes to mind is: why must the HTML structure behave like a value?
Almost every modern UI solution treats the UI as a value returned by the component. The obvious justification for this is to apply control flow over the UI and maintain a more vanilla feel. However, we have already comprehended this ideology’s limitations in a statement-oriented language.
Why not, like everything in nature, we adapt to our habitat and harness the power of statements by questioning the template primitive’s identity? and that is what NeoComp does using the builder pattern.
We have all used the builder pattern in some way in our lives. The builder pattern is any pattern that relies on the implicit construction of structures through an imperative API, the opposite of object composition, where the structure is built explicitly using native object initialization.
// Builder patternlet buf = [];buf.push('a');for (let i = 0; i < 10; i++) buf.push('b');if (some_cond) buf.push('c');let struct = buf.join(' ');
// Object compositionlet struct = build('a', Array(10).fill('b'), some_cond && 'c');From the example, you can derive several insights: the builder pattern is favored by imperative languages, while object composition is favored by object-oriented languages. Secondly, we didn’t even consider the builder pattern because of its verbosity, opting for object composition because it is more expressive.
Object composition is super expressive, however, even in expression-based languages, it has extremely rigid prettiness limits and, if not controlled, can transform into syntax monsters. Conversely, the builder pattern is less symbolically expressive but very consistent, and if you glimpse closer, you might notice its power.
NeoComp doesn’t just rely on the builder pattern for everything, since it is not influenced by Java. Its html literals are exactly like the beloved Solid ones with some dialect differences, except they don’t return the structure, the declared chunk is implicitly pushed into an internal, overarching structure.
let { html, signal, effect } = build;
let count = signal(0);effect(() => console.log(count.value));html`<div> <button on:click=${() => count.value--}>dec</button> <span>${count}</span> <button on:click=${() => count.value++}>inc</button></div>`;
let text = signal('');html` <input on:input=${(e) => (text.value = e.target.value)} /> <div>${text}</div>`;If you are still fascinated by this primitive example, let me discuss it thoroughly. NeoComp borrowed the lightweight declarative HTML declaration from Lit, fused it with the power of fine-grained reactivity popularized by Solid, and drove it with imperative construction based on the builder pattern, creating a unique harmony between the imperative and declarative paradigms, all totally in vanilla JS.
The Powers of Chunked Construction
Section titled “The Powers of Chunked Construction”NeoComp calls this revolutionary innovation chunked construction, and it is provided by builders called ChunkBuilds. This approach to templating adheres to the core principles of declarative, reactive UI, however, it adds its own twist of expressivity.
The most obvious power-up is the localization of logic and structure. Since UI is composed of multiple chunks, you are not forced to comply with strict “logic then UI” blobs, nor the delegation to mini and nano components, you just lay them out sequentially, inlined with their logic, exactly like in the previous example.
In addition, since the UI is driven by imperative construction, you can use any imperative control flow and even any imperative pattern. Throw away the ternary madness or any other politely engineered primitive and just stick with the old pals: the if, for, while, and even switch statements.
for (let i = 0; i < 10; i++) { let count = signal(0); html`<button on:click=${() => count.value++}>counter ${i}: ${count}</button>`; if (i % 5 === 4) html`<br />`;}While the corresponding structure is static, not dynamic and as explained in the Solid section, the only places that demand complex control flow are the initial UI structure, not the simple bindings. And chunks can be created on demand, even inside effects.
As noted by Ripple, having an expression block placeable inside the template unlocks infinite merits of expressivity and freedom. To that end, NeoComp provides this functionality by just passing a closure as a placeholder using the syntax <${() => { ... }}>.
The do blocks are executed at their definition point in the structure, and calls to the html function also target that point. This transforms the chunk build from a linear builder into a tree builder where chunks can be nested infinitely deep, deepening with them the localization ability.
let a = signal(0);html` <button on:click=${() => a.value++}>a: ${a}</button> <${() => { let b = signal(0); html`<button on:click=${() => b.value++}>b: ${b}</button>`; html`<div>a + b = ${() => a.value + b.value}</div>`; html`<div><${() => { let c = signal(0); setInterval(() => c.value++, 1000); html`a + b * c = ${() => a.value + b.value * c.value}`; }} /></div>`; }}>`;Components were marketed as the revolutionary building block that started the modern JS revolution. With them, millions of mechanisms, patterns, and rules arrived just from their mere existence. We accepted this reality as a regular cost of modernity, yet in NeoComp, there are no Components, since there is no need for Components.
The do block nuked every existential justification for nano Components, and the imperative chunked construction killed every convenience of utility Components. NeoComp is chunk-oriented where you can pass the chunk build with complete freedom to any depth. The remaining, real components are just normal functions that borrow the chunk build, not an abstract entity.
// not a Component, but a componentfunction counter (build, name) { let count = build.signal(0); build.html`<button on:click=${() => count.value++}>counter ${name}: ${count}</button>`;}
html`<div><${() => { for (let name of ['a', 'b', 'c']) counter(build, name);}}></div>`;Despite all this expressivity, NeoComp’s chunk builds function like primitive utilities. Chunks can be built on demand with no specific rules, scopes, or order, simultaneously, asynchronously, and even recursively. Many advanced functionalities that typically demand complex built-in utilities or sophisticated patterns are achieved here with the regular imperative logic in pure elegance.
let pending = build.signal(true);let async_build = build.ctx.new_chunk(document.createElement('div'));build.html`${async_build} ${() => (pending.value ? 'pending' : '')}`;
// Execute in parallel(async () => { let data = await fetch('/data.json').then((res) => res.json()); async_build.html`${data}`; pending.value = false;})();Lightweight Vanilla
Section titled “Lightweight Vanilla”Staring at NeoComp’s examples enriches you with the old-school vanilla vibe. It feels like returning to your home village after decades of living inside a crowded city. Yet, these are not just ordinary feelings, there are real and forgotten abilities in being just vanilla.
The primary one that comes to mind is direct runnability. While, like every modern project, NeoComp is written in TypeScript, yet a quick unconfigured dev server, bundler bundle, or any minified version lying on a CDN across the globe gives you a quick hop into coding NeoComp apps with a full feature set, without a mandatory build step annoying you.
It is true that the majority of codebases use the example config from the official framework docs and call it a day. However, the question is why an interpreted language community finds it normal and regular to require production-level compiler infrastructure just for a simple counter example.
Even though NeoComp is featurefull, having an expressive HTML syntax, an advanced reactivity system, and many chunk utilities (like list rendering), its size is minuscule, weighing 3 KB gzipped for a counter example, 4 KB gzipped for the full feature set, and 5 KB gzipped if runtime chunk parsing is enabled.
Moreover, add to all this architectural flexibility DOM flexibility. When a chunk is committed, you gain total control over it. You are free to reorder the elements as you wish because, thanks to fine-grained reactivity, there are no hidden internal mechanisms working on the generated DOM, and the bindings target the elements by reference.
Who needs portals when transferring a chunk across document boundaries works perfectly fine, exactly like any other ordinary thing?
let exported = build.ctx.new_chunk(document.createElement('div'));let count = exported.signal(0);exported.html`<button on:click=${() => count.value++}>${count}</button>`;
let iframe = document.createElement('iframe');build.html`${iframe}`;// Just an ordinary dayiframe.contentDocument?.body.append(exported.base_el);Featuring this vanilla lightweightness turns NeoComp into a good candidate for islands and micro-frontends. To support that, NeoComp provides a context-based approach where multiple UI systems can coexist completely independently on the same page, each having its own bounded reactivity system.
There are no global systems whatsoever, just local, fully deterministic UI contexts that own everything inside them, even their reactivity primitives. This is not a limitation, this is architectural freedom.
<!-- Old-school scripts --><script type="module"> import { Context } from 'https://some-cdn.com/neocomp.js';
function widget(chunk) { let { signal, html } = chunk;
let count = signal(0); html`<button on:click=${() => count.value++}>counter: ${count}</button>`; }
let context = new Context(document.querySelector('.some-widget#w123')); widget(context.root_chunk());</script>// Modern moduled apps// context.jsimport { Context } from '@neocomp/core';
let context = new Context(document.querySelector('#main'));export const { html, signal, effect, computed } = ctx.root_chunk();
// counter.jsimport { html, signal } from './context.js';
export function counter(name) { let count = signal(0); html`<button on:click=${() => count.value++}>counter ${name}: ${count}</button>`;}
// app.jsimport { html } from './context.js';import { counter } from './counter.js';
html`<div><${() => counter(1)} /></div>`;The general essence of NeoComp gives you multiple vibes: the old-school vanilla vibe, the modern architected vibe, and most weirdly, a low-level vibe.
An Essence of Low-Levelness
Section titled “An Essence of Low-Levelness”To begin, I am not a web developer, I am not even a mid-range developer. I am a person who dedicated their life to logic design and language design.
What made me develop a high-level JS framework? It was on my shelf from my old web days, sounding like decades ago but actually ~2023.
This is, in essence, a weekend port of a Rust UI framework called NeoView. NeoView is NeoComp, but lower-level, more robust, and more rusty.
fn counter(build: &mut ChunkBuild, name: &str) { let count = build.signal(0); chunk!(build, button( on.click: (move |ctx, _| ctx.update(count, |v| *v += 1)) ) { format!("counter {name}: "), count });}
let mut build = ctx.root_chunk();chunk!(build, div { h3 { "Hello world!" } for name in 'a'..='c' { counter(build, name); }});build.build();This Rust port background explains every thought you have on why the framework is full of Entity Component System patterns, imperative intrinsic optimizations, and a minimal dose of modern magic. The old-school scripter mentality is the default mentality of a low-level engineer quickly hopping onto the web.
While this new iteration of NeoComp is a Rust port, NeoComp actually has a rich history and multiple previous iterations summarized by web_history.reduce(v => v !== 'vdom').shuffle().join(). It evolved from a Backbone era monstrosity to reasonable mid-verbosity component classes, to the latest and most practical, craziest cocktail of paradigms.
You had mid-verbosity component classes with Backbone-vibe APIs, deep-state fine-grained reactivity with imperative chunked construction, and there were no do blocks. You would just break the template at any point, do some calculations, then continue normally like nothing ever happened, except your teammates would be traumatized after reading your code.
// A middle ground for everyoneclass Counter extends Component { constructor() { super(); const { html } = this.createTop();
html`<div>`; for (let name of ['a', 'b', 'c']) { let count = this.signal(0); html`<button on:click=${() => count.value++}>count: ${count}</button>`; } html`</div>`;
this.fireInit(); }}JavaScript, Quirky Beauty
Section titled “JavaScript, Quirky Beauty”It is impossible to emphasize the sheer absurdity of JavaScript. How a language a brainf*ck-style language was an emergent property of its quirkiness yet casually powers the entire web, a language where immutability is mutable, yet optimizable to bare metal, a language where bugs are at line -42, and its typed superset can run Doom in its types, a language built for exotic madness, the language of next-gen expressivity.
The language that encompassed the modern web revolution, yet we established sophistically complicated mechanisms, compiler infrastructure, language extensions, overengineered patterns, and abstracted dark magic just to work with it while we hide on another planet.
Why use JavaScript while not using JavaScript? Why build towers, fortresses, and bastions around a language whose quirkiness is deterministic? If it works, it works, and its quirkiness is a side effect of its expressivity and forgiveness.
While {} + [] === 0 is devilish logic and a trillion-dollar mistake, all of JS’s quirkiness is a side effect of implicit conversions created to help non-programmers create their sites with ease (1 - '1' == '0'). And what remains is a very capable, expressive, and powerful language that can do what mandates deep runtime interfaces in other languages, using regular, normal JS.
if (!globalThis.Temporal) globalThis.Temporal = TemporalPolyfill;NeoComp is a genuine call to harness JavaScript’s uniqueness for more benefits, and not escape from it with overcomplicated abstractions that only overload the mind. By being humble like the humble JavaScript, we can merge all of our innovations in declarative reactivity with powerful imperative chunked construction, creating a unique harmony between the imperative and declarative paradigms.
And the twist? Bring back these characters: .value, () => {}, html` `, $, build.
The End
Section titled “The End”I hope you enjoyed this very long yet informative ride through the history of web dev and the heart of framework design. And I hope you take a little taste of vanilla JavaScript to colorify your ideologies and mentality.
As for the future of NeoComp: for now, it is just a non-production-ready prototype and a side project capable proof of concept. It will be hardly maintained since I am still an undergraduate student under development, yet it is in good shape for now.
I would adore hearing your thoughts and feedback on this new paradigm, and if you like this work, you might be interested in viewing more of my projects.
Ah… My last work before NeoView was UniMap, a pure pattern-matching language, and the last web-related one was TEEP, a CPU implemented in TypeScript types.
wish(new Time() |> filter((t) => !t.includes(bugs))) & nice(20, bye);