After I finished the first article I wasn't sure how long it would be before we'd have enough topics for the next round. But given the continuing atmosphere around the JavaScript ecosystem, many of these topics just fell on my lap.
So let's get this started. Fight!
1. Build vs No Build
Several years ago I read a great article(which I can't seem to find anymore) that saw JavaScript being at a crossroads. That "the language" of JavaScript was at odds with what the author considered "the machine". I barely appreciated the nuance in the take but standing here now it all makes sense.
For some of us old-timers, our first introduction to JavaScript was through viewing the page source on public websites. We saw something we liked we'd just copy it. And the web being the open platform it is we embraced this freedom.
Fast forward 15 years later and we're trying to support some ancient browser while lamenting all the new features we know are coming out. So what do we do? Compile. CoffeeScript, and later Babel. And our desire to build native experiences has meant more and more JavaScript so we minimize and we optimize our bundles.
And now over 25 years after the launch of the web, we've been doing this for over 10 years. Almost half the time JavaScript has been around. So which is the truer JavaScript?
I've been calling JavaScript a meta-language for several years. We use JavaScript to rewrite how other JavaScript executes to the point now, that looking at some source code you see in a random Github repo, you don't really know what you are going to get by the time it gets to the browser.
Is that good or bad? Not sure. But it's definitely a thing. The ecosystem has become ingrown feeding off itself. Machine writing the machine. Our compilers are more advanced. Our tooling is more sophisticated. Do we even know how to write without these? Do we even need to?
Well according to some it's time for the great collapse. The native platform has improved greatly over the same period of time and is more capable than ever. But can it dead code eliminate all the concurrent mode code out of the core routines of a reactive library based on what the end application imports? Can it analyze cross templates how all state is used in declarative views to determine exactly what JavaScript needs to be sent to the browser?
No. Not even close.
Ever wonder why Bundle-less tools like Vite still pre-bundle node_modules
? How Svelte can resemble plain HTML, CSS, and JavaScript but do so much more? Tooling is becoming so ingrained we don't even think about it. It keeps "shifting left" moving right into our IDEs.
But that might be alright if you don't need any of that.
As the author of the original article concluded it's too late to stop the machine. What tools offer us paint a world of developer experience we can only imagine in our dreams. We don't need to give up UX for this DX either. More tools don't mean more JavaScript in the browser. In the next several years I expect it to mean less. Much less.
But what is great about the web is we can always just turn it off. index.html
anyone? You won't find me in that lineup. But who knows with import maps and native ESM you might even feel like exposing your unminified source to inspire the next generation of JavaScript developers anew.
2. Framework Specific vs Framework Agnostic
Framework Agnostic. I mean that's the dream, right? We've been trying to achieve this for decades. So why aren't we here yet?
As much as people we like to generalize, if frameworks were all the same there wouldn't be so many. At best being framework-agnostic is catering to the lowest common denominator. It isn't just about syntax. React's concurrent mode, Svelte's animations, Marko's automatic partial hydration, and progressive rendering... the list goes on. The wider the net the harder it is to generalize and to optimize.
And these differences are usually rooted in fundamental philosophies and architectures. Every decision has tradeoffs and we can't expect everything to align. Even when you have things that look as similar as React's Hooks, and Solid's Reactivity. Every decade or so the trends and the patterns change and adapt. Would you be happy to use jQuery plugins in your declarative modern SPA framework?
So what does framework-agnostic actually mean? Well, it just means we have a new framework. This is a great approach to gaining adoption and it can consolidate on like things. If all things are equal wouldn't you take the most efficient option? If you were going to use Web Components to create your cross framework micro-frontend design system would you author them in React? Would you use Preact perhaps?
Over time the agnostic thing will naturally consolidate on the most optimal underlying means that most align with its philosophy and goals. And at that point, you have to evaluate whether the wrapper actually adds sufficient value.
The thing is it is always possible to write a better version of a framework-agnostic thing when you make it specific. Interop definitely has value and a sense of future-proofing, but done wrong it's akin to over-engineering. The avarice that we can predict the future.
Even true when sufficiently complicated things are presented as standards or part of an official platform. As long as there are options some people number of people will gravitate to the most efficient way to accomplish something or what fits the guiding principles they are working under.
There is nothing wrong with that and like any pendulum, we really need both parts, experimentation/growth, and consolidation/standardization. Just, I wouldn't be so sure that either side presents more stability over the long term. Ultimately all things need to be adopted to ensure survival.
3. Language Primitive vs Composition
Composition is King. Or was it "Never bet against JavaScript composition"? As a framework author this is something I hold near and dear. Composition lets you build simple and extendable things without adding complexity. Being adaptable is much better than being flexible.
So what does this have to do with JavaScript. Well, frameworks have been working hard at reducing the experience of describing UIs to first principles. Regardless of approach frameworks have landed on the language of reactivity as those building blocks. Call it reactivity, hooks, composition api, they all have 3 concepts:
State - observables, refs, signals, atoms
Derivations - computeds, memos, selectors
Reactions - effects, autorun
Even if we don't name it like Svelte, we have the same 3:
let x = 0; // state
$: y = x * 2; // derivation
$: console.log(`${y} is double ${x}`) // reaction
So are all of these the same? Well not quite. Svelte went the way of language keyword versus React's use of functions. It isn't unlike the difference between:
for(let i = 0; i < list.length; i++) {
doSomething(list[i])
}
// and
list.forEach(item => doSomething(item));
What's the difference? Well once you want to abstract our list iterator here you can no longer call it with the same for
syntax. Instead we need to use functions like forEach
. In fact you can make myForEach
with the exact same signature. And do so ad nauseum.
Know what else is composable? Components. They weren't always part of the frontend framework landscape, but since their introduction have been ubiquitous. You might use list.map
somewhere but <VirtualList>
, <PaginatedList>
also extend the pattern forward in a composable way.
Like for
loop, template helpers like Svelte's #each
are language level instead of composable. This allows for a dedicated and clean syntax. But when you move to <PaginatedList>
it requires a completely different syntax(Slot Props). And Svelte isn't alone. Most template DSLs have gone this way with their control flow.
So who cares? Well there is something incredibly powerful about the framework provided primitives and the end user creations being the same. It provides a simplicity in its consistency and makes extensions feel native. If you've used useState
you know how to use useLocalState
. If you've used <For>
you know how to use <PaginatedList>
. It isn't special.
The best part is if you don't like what is provided with composable APIs you can create your own and have it feel first party. It lets developers own their experience and drive the ecosystem entirely on what fits their usage. React's Components and Hooks have been incredibly successful for this reason. And though I've been using Svelte as the counterpoint, Svelte Stores are good at this as well.
Language level primitives do have benefits. They are often easier to analyze which lends to compilers being able to optimize. So I look forward to seeing how developers look to incorporating the best of both worlds without too much compromise. So far that's things like Vue's ref sugar and Marko's Tags API. It's definitely something to keep an eye out for.
4. Runtime vs Runtime-less Frameworks
Ok, I admit it. This one is complete bait. It would be interesting to see a truly runtime-less JavaScript framework with a significant feature set. But these don't really exist and for good reason.
To clarify, I'm not talking about frameworks that enable not sending JavaScript to the browser. I am talking about ones that make heavy use of compilation to do work ahead of time. The term "disappearing" or "runtime-less" has floated around but it is hyperbole.
Re-using code is good. You don't want to compile away the framework completely as that can lead to a lot of duplication that doesn't scale.
More often than not compiling away the library just moves the size from one place to another. Often some of the savings you make in the runtime translate into larger components. For that reason, Svelte who is the poster child for small runtime is also the poster child for comparisons on how Components scale.
React/Svelte Size Analysis
Vue/Svelte Size Analysis
30 TodoMVCs or 19 TodoMVCs is still a steep curve to reach size equivalency so obviously, this is net positive on size for the most part.
And making this trade can have other benefits. More specific code can be more performant than generalized code and at each touchpoint be less code than the equivalent generic method. Obviously, these can add up, but these sorts of tradeoffs are often worth it.
Alternatively, there are frameworks that are completely runtime that have even smaller base runtimes. HyperApp advertises a size of about ~1kb, which is even smaller than Svelte's ~1.6kb. So there is no hard and fast rule.
In fact, it's become a mostly pointless exercise even looking at bundlephobia.com sizes. Modern tree-shaking and dead code elimination can produce sizes noticeably smaller than advertised. But probably more important is that a pattern you find in many frameworks of using submodules like preact/hooks
, svelte/motion
, or solid-js/store
don't count towards the advertised bundle size.
So the takeaway is the only way to really know framework size is in examples of where it is being used. Marketing buzz like runtime-less is mostly irrelevant to the conversation on size. There are small libraries and there are smaller libraries.
5. Progressive Enhancement vs Hydration
I hear the term progressive enhancement thrown around a lot. Although the first place I really acknowledged it was the promise of Web Components. The idea is that you could define extra behavior and fallback to native behavior if say the browser didn't support certain features or JavaScript was enabled. Sadly, due to Apple blocking progress on native built-ins it's pretty hard to consider Web Components successful here.
Nowadays, I hear this term to mean adding small amounts of JavaScript to enhance a server-rendered page. Things like Stimulus, Alpine.js, or Petite Vue. And admittedly these can be progressive enhancement but they can also not be. Simply adding JavaScript to add functionality to a page doesn't mean it works fine without it. There is no guarantee a button works as intended without the JavaScript.
Conversely, anyone who watched Svelte Summit this year or saw the Remix Run beta preview video, knows these frameworks were showing off fully functioning sites with all JavaScript turned off even though they are full-blown Single Page Apps. Sure it's Svelte or React but that's progressive enhancement in my book.
Alpine.js, Stimulus, and company are ultra-small frameworks that are built for adding JavaScript functionality on top of server-rendered DOM nodes instead of more substantial client-side rendering. But all JavaScript frameworks that server render do this as well. They just call it "Hydration".
A framework might run top-down instead of doing the independently targeted hydration these smaller libraries do but that is more of an implementation detail. Same steps of initializing client state and attaching event listeners.
And that's it. Progressive enhancement is a consideration like Accessibility. A choice we make as developers to implement rather than a specific technology. Hydration is an inevitability that comes with adding JavaScript functionality to server-rendered nodes. The only thing that matters is how much or little of it we do.
This brings us to...
6. Partial vs Progressive vs Resumable Hydration
So there are 3 things here. Or is this one thing. That's the whole problem. I'm sure some of you are just thinking this all makes you thirsty. Just how hydrated should our JavaScript be?
All joking aside, this is an area with a huge amount of confusion and the reason is instead of naming a specific technique these are loosely-held descriptors. They describe a characteristic of the approach but not how it works. Regardless of the solution, we all need to do it to have interactive JavaScript on our server-rendered pages.
Partial Hydration as the name implies is that not all of the page needs hydration. In practice, this means we don't need to send all of our component code to the browser. The term "Islands" gets used a lot and it makes sense as the most common approach involves breaking the app into a static page with only these "Islands" of components we need to send to the Browser.
Progressive Hydration is the ability to hydrate the page as needed. Maybe when it comes into view or maybe on interaction. Even if you end up hydrating the whole page, by breaking up Hydration we can reduce the time spent during the initial load.
Resumable Hydration is a technique to reduce the execution time during Hydration by serializing the data needed at a local component level to skip the need to do any calculations at hydration time. In so Hydration's only job at that point is to add event handlers.
These techniques are all not mutually exclusive. Astro leverages Partial and Progressive Hydration through its manual Islands approach to extend this capability on top of familiar frameworks. Qwik has been pioneering Resumable Hydration alongside its use of Component level Progressive Hydration to do a pretty good impression of Partial Hydration without explicit Islands. Marko has long used its compiler to automatically detect Islands but has been incorporating Resumable Hydration as a means to ship the least possible code to the browser.
But what this all means in a mechanical sense is a lot less clear. It comes down to what gets hydrated and when. There is a very fine line between deferring work until later and never doing it all. Is your hydration truly resumable if you need to redo work that was done on the server the second your component updates? Is backloading JavaScript to when you interact with it a different sort of cost metrics today aren't capturing?
You might have noticed the lack of mention of the common solutions in this section like React, Vue, or Svelte. And part of that is Multi-Page App Frameworks are dominating innovation in this space given Single Page Apps are less optimizable in this way. But VuePress' split bundles, and React Server Components are looking at ways to apply some of the savings there too.
And that does it for another installment of JavaScript vs JavaScript. I'm sure it won't be long before new topics present themselves in this ever-evolving ecosystem.
Top comments (12)
Awesome post! I wish i could find that article you were talking about โ It sounds like an awesome read!
Guess what: I'm experimenting with building such a framework!
The closest I've seen is Prism. It isn't completely runtime-less in general but they had a cool webcomponents demo that essentially was since all stateful updates in the simple example were simply handled by getter/setters on the element.
In Clojure I came in contact with REPL-Driven Development which coming from a C/C++/C#/Java background was absolutely foreign to me (apart perhaps from working in an SQL shell).
Similarly with JavaScript, at least on desktop browsers, through the Web Developers Tools console one can interact live, directly with the browser and the page or even just simply locally throw together an
index.html
to play around with some ideas.Sure, you can go to the TypeScript Playground to play with some TypeScript but that doesn't have access to the browser; sure one can use services like CodePen or CodeSandbox but one typically has to figure out the idiosyncratic details of their UI and platform.
Yes, it's not that difficult to "just" download
create-react-app
(if you're comfortable withnpm
), though but, gee โฆ(I typically just use Preact).
Point is, having an incredibly low barrier to experimentation is invaluable (I assume that's why framework websites often have playgrounds associated with them).
Sure, tooling is bound to aggregate around products as they move closer to production but it can be really useful to have an extremely low barrier to just getting started.
I think Ramus Lerdorf summarized it well:
People were worried about vendor lock-in with application servers, now they are worried about vendor lock-in with cloud service providers , meanwhile mindset lock-in is in full swing with frameworks. I can't even imagine how irritating it must be for authors of the "other" frameworks, to constantly have to explain themselves in terms of React โ when React isn't even of the web โ in fact it was created to de-emphasize the web.
Perhaps it's developers who need to be "framework-agnositic". The problem is that adopting any particular framework (of any kind) represents a significant investment of time and effort, naturally pre-disposing one to fall victim to the golden hammer rather than pick the right tool for the job. The typical "enterprise response" is to settle on one "batteries included" solution and standardize on it โ however that often leads to added baggage which can typically be absorbed in the enterprise space but often not so much on the public internet.
Mmm โ that is highly context dependent.
The wider and more complex the interface of a component (in the general sense) is, the less composable it is - a component is an object after all.
Joe Armstrong:
(primitives in Erlang: spawn, send, receive, make_ref, etc. โ the (composable) mechansims are built with functions, tuples and lists.)
So perhaps some framework "primitives" are just poorly chosen?
From that perspective React would already be a mechanism on which other custom mechanisms are built.
Components (in the React or JSX sense) can require and support a vastly varying set of
props
(andchildren
) depending on their role.For example
<div>
plays are very different role from<CountContext.Provider value={value}>{children}</CountContext.Provider>
and require a very different understanding to be used effectively. From that perspective the composability of "Components" is purely a result of by design being constructed to collaborate within and through the component tree via an implicit interface. That's OOD 101.Design Patterns: elements of reusable object-oriented software; 1995, p.18:
And implementing framework specific types is par of the course with frameworks.
In some sense compilation is a responsible means of providing "developer experience" because it can create value without deferring the cost further downstream. Often "run time is cheaper than developer time" is the justification for deferring cost further downstream but certain forms of compilation can help to mitigate that.
I think to some degree that is only part of it; Everyone has JavaScript Right?
i.e. don't design an application that will only work under optimal conditions; deliver functionality with the minimum amount of HTML/CSS and JavaScript to engage the user as quickly as possible, then carefully and judiciously download and cache additional capabilities.
Putting a different spin on this: SPAs introduced monoliths to web applications.
The history of SPAs seems to go back to about 2003; meanwhile in 2005 microservices became a thing, largely leaning on the idea of a bounded context from Eric Evans's Domain-Driven Design in 2003.
In 2015 I vividly remember running into blog posts like Mixing MPA and SPA: worst of both worlds.
Then Micro Frontends made an appearance in 2016 - which in my judgement completely failed to take the nature and constraints of the web into account - especially when claiming the "benefit" of micro frontends being able to each use their own distinct/separate UI framework.
Perhaps MPAs will become the real "micro frontends" โ each page having highly focused capability which can be downloaded relatively quickly while sharing resources with other pages from the site via Service Worker, Indexed DB, and BroadcastChannel (SharedWorker would be nice).
Holy s**t bro, I read till the end of the article, your post is awesome, i would like to read more about this stuff next time!
bostonmagazine.com/news/2021/09/14...
โBut thereโs so much stuff to do,โ he continues. โThereโs so much work to do now trying to clean the Web up. For Solid, thereโs a serious amount of work. Thereโs never very much time to sit back and go, โOh, wow.โโ
I would love to read your thoughts about this epic battle for the WWW as a whole.
Where's part 3? ๐
Nice article....
Hah.. It's only been a couple days.. need a few more things to gather up. MPA mode maybe. Seriously this year has been ripe for these sort of topics. So much new tech coming up in SSR, rebirth of MPAs, and Svelte's philosophy permeating the frontend hitting a critical mass.
Now: MPA is comeback! You can build website with 0% JS!
Later: Today, you can build website with 0% HTML ๐
If you like Svelte, you should take a look at RiotJS - been around a lot longer - it's on version 6 now and seems to have influenced Svelte a lot
I read your posts only for memes.
Love your Articles Ryan.. ๐โกโค๏ธ
Love the mention of hyperapp! Wish I could get more traction with it at work