With software development, we are often presented with alternative choices for libraries and tools that appear to accomplish the same sort of things. Each one will advertise its key advantages, and we attempt to weigh the pros and cons.
Sometimes, the differentiators have less to do with what we are accomplishing and everything with how we accomplish it. In those cases, it isn't always as clear what the tradeoffs are. Are these things that even matter?
Single Page Apps vs Multi-Page Apps is by far one of the biggest sources of confusion I've seen for the web. There are so many different patterns that exist for building websites and applications it is far from clear what people even mean by these terms.
Your bundler might produce different chunks per page but if your application starts from the same point regardless of the page you have a SPA. You can pre-render it into 1000 different pages. You can preload the per page chunks for each. You can even turn off client-side routing. It is still a SPA architecturally. Single application execution defines behavior for all pages.
It's worth mentioning, MPAs are each pages themselves so they could always host a SPA on a given page. And through iFrames or other HTML injection frameworks like Turbo, it is possible to serve an MPA off a single page.
The key takeaway is that SPA vs MPA isn't about how many pages you are serving. It is a decision you make depending on how important initial load performance(MPA) is versus future navigation experience(SPA). Either approach has tools to improve their weaknesses but each is fundamentally tailored to optimize for their primary usage.
You've probably heard somewhere React is not reactive. Maybe someone thought it was a funny joke. You know, React is in the word Reactive. Maybe you read a blog post that got into the fundamentals of push-based observables versus scheduling. Maybe you saw a framework advertise itself as "Truly Reactive" or "Purely Reactive" as a differentiator from React.
Here is the thing. There have been many attempts to formalize what Reactive programming means. Some are more narrow than others. So much so that even within reactive circles we've needed to differentiate between "Functional Reactive Programming" and "Functional + Reactive Programming".(source)
The common thing driving all these solutions is they are systems based on declarative data. You code in a style that describes the state in terms of fixed relationships. You can think of it as equations in a spreadsheet. The guarantee is that with any change everything stays up to date.
If this sounds like pretty much any web UI development you've worked with it's for good reason. HTML is declarative and we build on top of it. At this point in terms of frameworks being reactive means any number of things.
Some take it to mean you have control over the primitives to wire up behavior, but it would be hard not to consider React Hooks in this way.
Some take it to mean that updates happen automatically without calling an update function, but these libraries like Svelte do in fact call component update functions behind the scenes.
Some take it to mean granular updates without diffing. But every framework diffs (more on that below). Or that it means we remove scheduling but almost all frameworks batch changes and schedules them on the next microtask.
So React might not be formalized reactive programming but for all effective purposes, the same things are being accomplished in, perhaps surprisingly, almost the same way.
Believe it or not, this process is similar in the majority of frameworks. The reason is reading values from the DOM does not come without consequence. In the worst case, it can even cause premature layout calculations/reflows. So what do we do? We store values outside of the DOM and compare those. Has it changed yes? Apply updates. Otherwise no. This is true of all libraries VDOM or not.
But how we go about this is where the differences lie. There is 2 axis along which solutions differ:
- Granularity of change - How much do we re-run in response to the user changes
- What are we diffing - data, VDOM abstraction
For a VDOM library, like React, the granularity of change is per component. On the first run the code you supply to render functions or function components executes and returns a bunch of Virtual Nodes. The reconciler then creates the DOM nodes from that. On subsequent runs, new Virtual nodes are diffed from the previous Virtual Nodes, and updates to the existing DOM nodes are patched in.
For a non-VDOM library, like Svelte, the granularity of change is also per component. This time the compiler has split create and update paths. On the first run, the create path creates the DOM nodes and initializes locally stored state. On subsequent runs, it calls the update path which compares the values of state and patches the DOM where applicable.
If these processes sound incredibly similar, it's because they are. The biggest difference is that the VDOM has an intermediate format for diffing instead of just a simple locally scoped object and Svelte's compiler just compiles only the needed checks in. It can tell which attributes change or which locations child components are inserted.
These traversals don't need to be expensive. You can apply similar optimizations to VDOM libraries as we've seen with Inferno and Vue using compilation. In so they prevent recreating VNodes similar to how a non-VDOM library avoids unnecessary creation of DOM nodes. It is all about memoization, whether of VDOM nodes, data objects, or through reactive computations.
So what is the meaningful difference? Not very much. Diffing isn't that expensive. The only thing left we have to play with is the granularity of change, if ideal diffing and updates are about the same cost all we can do is do less diffing. But granularity generally brings heavier creation costs. Thankfully there is a lot more optimization to be done with compilers to address those creation costs.
So where is the difference? Mostly around what can be inserted. Attributes are easy to analyze and optimize, but what goes between the tags in JSX could be a few things. It could be text, it could DOM elements, it could components or control flow. Ultimately though it's text or DOM elements.
The biggest benefit of Template DSLs is the explicit syntax for control flow can make it more easily optimizable for different targets. For instance, a
for loop is more optimal than
map. If you are rendering on the server just creating a giant HTML string, something as small as that can improve performance by a noticeable amount. But that is just a simple abstraction.
But outside of these sort of scenarios there really isn't any differences fundamentally. Sure most Template DSLs don't have equivalent to React's Render Props, but they could. Marko does.
This one might be a bit more niche but it's still a question I get often. What's the difference?
It comes down to dependency tracking. Runtime reactive systems like Solid, MobX, or Vue collect dependencies as their computations run. They intercept reactive atoms(signals, refs, observables) reads and subscribe the wrapping scope to them. So that later when those atoms update they can re-run these computations.
The key thing is that since the dependency graph is built on the fly they are dynamic. They can change run to run, and in so you are managing a set of dependencies. Every run means potentially new subscriptions and releasing others.
Compile-time figures out the dependencies ahead of time. In so, there is no need to manage subscriptions as the relationship is fixed. The code runs whenever the dependency changes. This has far less overhead at runtime. And even means that computations don't need to run to know their dependencies.
However, these dependencies aren't dynamic so there is always a chance of oversubscribing and over executing. Situations, where runtime can pull reactivity from a deep call stack, become harder because you can't know if something is reactive without tracing its bindings.
This works both ways though. If you were to put a setTimeout in an effect, with runtime reactivity by the time it executes it would not be in scope. With compile-time, if it's inside the function it's easy to register the dependencies. Similar considerations when you update values. Svelte looks for an assignment operator which is why
list.push doesn't just work.
There is a lot to be done with the compiler and it is easier to do some things than others. In the end, for the most part in terms of raw performance, this is mostly a wash. But a compiler can bring a lot of other benefits when you can leverage specific syntax to better convey intent. This is the next logical step over template DSLs and I think we are just scratching the surface here.
I want to open this by saying if there is one takeaway here, don't assume the word component means the same thing to everyone.
I've been on both sides of this. I used Web Components in production for 6 years at the startup I previously work at and I've also worked and written Component frameworks. To me when people compare these they are talking about very different things.
A Web Component is very much a Custom Element in every sense. It is a DOM node that encapsulates behavior with a common interface. We get attributes and convenient hooks to write custom behavior on creation, and when attached and removed from the DOM. The latter 2 lifecycles are important because they are complementary. It means that for the most part all side effects are tied to DOM connectivity.
What is a Framework Component? To paraphrase something Rich Harris once said, they are tools to organize our minds. Now that's abstract. But that's the thing. When you look at components in frameworks, they are abstract. They might output DOM elements, or not. Their lifecycle is not tied to the DOM. Some manage state, they are rendered on the server or maybe mobile. They are anything the framework needs them to be.
The first is an interopt story, the second an organization story. Are these congruent goals? To a point. But neither will compromise on their primary purpose. And in so they are destined to stay in their lanes. I mean you can add more framework-like behavior to Web Components but then you become a framework yourself and no longer are standard. But as soon as you take the solution farther, like SSR you are inventing new territory.
This can be argued to be ground for new standards but I'd argue that standards development isn't an exercise in building a framework for the browser. Opinions change, technologies evolve. In the web, the DOM may be timeless but the way we organize our applications is not.
On the framework component side, there is no lack of incentives to take the abstraction further. The truth of the matter is a specific solution can always be more tailored to the problem. And in so a React Component is always going to be capable of being better in a React App than a Web Component. And this is true for any framework.
This isn't a "use the platform" or "don't use the platform" argument. Web Components have a role to play as long as we clearly understand the difference. Every framework uses the platform. Some are just better than others. And some better than Web Components can. After all Web Components are just Custom Elements. And sometimes more DOM nodes are not the solution to the problem.
It was fun to dive into a few topics that I've gathered opinions on over the years. If you have any ideas of similar comparisons let me know in the comments.