React is Slow.
How many times have you heard that? You can't count. It is known that React and other frameworks, especially VDOM-based ones, have a lot of performance issues. But have you asked why?
To answer this question, let's look at the beginning of the modern age of frontend frameworks to see what really went wrong.
The web before React was simple; most websites were static/server-rendered pages, with some JavaScript sprinkled on top for simple but good interactivity. But if you wanted to do any advanced work on the client side, your options were, compared to today, a little less reactive.
Pre-React frameworks like Backbone and others were classical OOP-inspired MVCs. They focused a lot on the code structure and the data layer, and less, if at all, on the view layer and data binding.
When you wanted to update a DOM node, you would use the limited templating provided by your framework, or you would use JQuery to query the node and update it manually.
And then, React came to simplify web development and make declarative reactive UI the norm, and for that, it came up with a revolutionary concept.
The render function is, by definition, a function that recreates the UI structure on each update, and it is the only one responsible for that.
While render functions were introduced before React, the web started from static pages rebuilt on the server on each update, and many client frameworks after that implemented similar technologies like templates. React optimized it and made it more "JavaScript-y."
React adopted a technology similar to old string templates—HTML-syntaxed templates inline in JavaScript—but what sets it apart is the fact that they correspond to normal JavaScript values. They can be passed normally between variables and functions like ordinary JavaScript values, which allowed the UI code to be fully declarative with minimal syntax changes.
After React allowed writing UI in a factory paradigm with full reactivity, there were serious performance problems with re-rendering the entire app on each user interaction. It was impossible to accept this concept in production, but the React team came up with a solution.
VDOM is a lightweight representation of the native DOM; it doesn't do anything on its own. But when integrated into a render function so that it creates VDOM, not native DOM, some critical benefits emerge.
Other than being lightweight in usage, VDOM allows you to avoid re-rendering the entire app on each update by comparing the current VDOM with the previous one, and then updating only the changed parts. This is called VDOM reconciliation, and it is the main reason why React is possible.
VDOM reconciliation is absolutely the reason that provided the modern web with the simplicity at scale we have today, but it wasn't magic. It didn't erase the problem, only decreased its appearance.
Render functions are performant at a local scale. Yes, they are inefficient compared to direct approaches, but the difference is negligible as our computers are computational beasts.
However, as ambitions grew bigger and bigger and the complexity of a blog website skyrocketed, a crack in the render function formula had appeared, and it was over-rendering.
In render functions, when a state is changed, all of its children re-render. While this is controllable in a local scope, it gets out of hand really quickly when working with global state, as any single interaction propagates inside the entirety of the app, re-running all of its logic. And it doesn't stop here.
Adding to the pain, the introduction of new sources of updates other than user interaction, like animation, scrolling, and hover, had led to the re-running of hundreds of kilobytes worth of code, often every frame, making many websites unresponsive on newer hardware and unusable on older hardware.
While over-renders are a common problem in the React world, there is another source of pain.
Functions are amazing for immutable UI, but that only exists in static sites. And since functions have no concept of instanced, persisted variables (class fields), the React team hacked a way for having states inside functions, and it "footguns the footgunner."
Inside render functions, you call API functions to get a snapshot of the state at the moment of calling. While this is fine inside the function's scope, it causes problems when using state in other ways, especially in closures.
Every core API and hook that accepts functions is given by users closures that capture the states, and since these closures can span more than one update, you use states from a past moment, not the up-to-date state, and good luck hunting bugs—spacetime ones.
Render functions are synchronous, and they run on every update. This behavior doesn't play well when dealing with code outside React; for that, every interaction with the outside has to be wrapped inside an effect.
Also, because of VDOM reconciliation, the entirety of the DOM is controlled by React and its internal systems, so any manual update to a DOM node created from a render function will at best be overwritten, and at worst, the DOM structure will be broken.
These reasons caused difficulties integrating React with many regions of the JavaScript ecosystem, especially other UI frameworks.
If React was easily fixable, the React team would have done that from the beginning. But the problem is in React's identity—its core concepts. Any attempt to fix the performance issues would have to break the core of React, requiring a major refactor of the React ecosystem and React-powered websites.
React's popularity made it restrained by the modern web codebases. Many frameworks tried to fix React, but they failed because migrating to an alternative requires an insane amount of manpower, and that is not affordable for all companies.
Even though React cannot change its fundamentals, it can be improved from the inside. And we can see this clearly as the React team has made milestones in terms of performance and scalability, especially with React Fiber and the React compiler. Batching of updates in idle and auto-memoization proved to be effective speed boosts without changing a letter.
Despite all of the above, React is still fast enough for most use cases. Over-rendering can be avoided completely with good practices and signals.
We are not developing game engines; we are developing websites with static structures and dynamic content. The problem is with streaming updates, and these require native approaches, not React.
Whatever happens, we must remember one thing: we are not discussing a quirky technology forced on us. We are discussing the framework that started and is driving the modern evolution of the web.
Declarative UI, JSX, model-view union, function-based components, server components, native mobile support, and many other revolutionary concepts were introduced by React to the webdev world and have become a standard.
React was the one that forced the hyper-evolving web we have today. It brings the newest, hottest concepts to the mass public, and the rest of the frameworks work on improving them. You don't know, maybe React's inefficiency was a good thing after all.
And still, React's core paradigm is flawed. Even though fixing it will break a lot of codebases, React must change. The core concepts are well-established in the community (signals, fine-grained reactivity...), what is required is a new, performant type of component that can coexist seamlessly with the old components. This is the secret recipe for fixing React.
Many frameworks tried to fix React in their own way, and everyone has good points, but the most promising one that is similar to React in syntax is SolidJS.
SolidJS is a JavaScript framework based on functional components and fine-grained reactivity, and it is the fastest widely used framework in the world.
It is very similar to React in syntax and related primitives; it uses JSX, functional components, and has similar APIs. However, Solid is faster, smaller, and more efficient and solid.
The key to understanding Solid's results is to understand how it works under the hood.
The core primitives in Solid are:
The component function is like a factory function; it is called once on creation and defines the initial states, signals, effects, event listeners, and constructs the initial UI structure.
After that, updates propagate through events to signals and effects, and lastly to the DOM by the effects defined in the template.
You would be wondering what the benefits of signals and effects are over the React way. The Solid way, also called fine-grained reactivity, allows efficient and direct DOM updates while adding no additional syntax.
If you noticed, the reactive bits of the code are coupled in effects, which creates direct update paths as only the required code is re-run. Also, signals allow the automatic tracking of dependencies while adding no extra syntax.
These factors allow Solid to have direct update paths with automatic dependency detection, achieving its goal of performance and declarativity, as opposed to React that needs to re-render the whole app since everything is coupled together.
While this technique is as efficient as manual updates, it is less expressive than render functions. Advanced features like conditional and list rendering require custom logic, while in React, they exist in native syntax.
The render functions allow the changing of the entire UI structure expressively, while fine-grained reactivity is stuck with a static structure. But here is the thing: do we really need this power? Our websites, from largest to smallest, are static structures with dynamic content.
The problem was the less declarative nature of the past frameworks. React came up with declarativity, but it was inefficient at scale. The rest of the world further refined and optimized it and are still fighting, while the problem has been fixed long ago by fine-grained reactivity, and that is the story of the modern framework war.
NeoComp also adopted the SolidJS concepts—fine-grained reactivity—but with a twist: it uses an object-oriented paradigm instead of a functional paradigm.
The functional paradigm is great for declarative, minimal-verbosity UI development, but it has some limitations for stateful large components, especially in terms of organization.
Before discussing the benefits of the object-oriented component over functional ones, let's dive into the different ways of implementing templates.