Cybernetically enhanced web apps.
A most interesting tagline for the Svelte framework since version 3. The Svelte team prides itself in aggressive compile-time automation and optimization—so much so that their previous tagline was actually "the magical disappearing UI framework" (literally!).
This is true enough even today, where the compiler strips away a lot of the inner mechanisms of the framework.1 Behind this philosophy, Svelte's creator Rich Harris believes that if the framework can analyze it anyway, then the developer should not worry about it—which subtly takes a stab at the more mainstream UI frameworks that do the work of state analysis and resolution at runtime instead. Such is the philosophy of cybernetically enhanced web apps: let the tooling do the work!
But how far does Svelte take this philosophy? Does Svelte go as far as to become a zero-cost abstraction?2 Putting aside all of the fancy compile-time machinery, how is the developer experience even like? In this article, we review the merits of the Svelte framework and its wider ecosystem.
Stellar Documentation
As with most things in life, first impressions also matter in the tech world. Poor examples, lengthy guides, inconvenient prerequisites, and strange syntax can easily turn off the most enthusiastic learners. As such, the classic Getting Started guide is where frameworks put their best foot forward.
In flying colors, Svelte really did put their best foot forward. The framework provides the following essential resources for onboarding:
Title | Description |
---|---|
Svelte | The official documentation/reference for the Svelte framework proper. |
SvelteKit | The official documentation/reference for the SvelteKit meta-framework. |
Examples | A repository of examples that serves as a tour of Svelte's features. |
REPL | A live REPL editor/playground for trying out ideas in Svelte. |
Tutorial | An interactive tutorial that features a comprehensive guide alongside a live editing environment. |
Needless to say, Svelte does not lack in documentation. The guides are comprehensive enough to be complete, yet concise and straight to the point. Furthermore, the tone and language are sufficiently technical, but not so much as to come off dry and full of jargon.
The best examples come from the interactive tutorial, where the guides walk the reader through a motivating example before introducing features. The tutorial thus feels like a journey of discovering the essence of Svelte rather than a lecture on Svelte.
Meanwhile, for quick experimentation, the live examples and the REPL are the easiest ways to test the framework's behaviors. In my experience, I've found the REPL to be an invaluable tool in my belt when prototyping stateful components. The REPL also doubles as my go-to resource for demonstrating Svelte's coolest features to my peers.
Low Barrier to Entry
Personally, the best part about Svelte is its low barrier to entry. Much like the approach of TypeScript and Vue.js, the Svelte language3 strives to be a superset of the familiar trifecta of web development: HTML, CSS, and JavaScript.
As a mentor to many of my peers, it is so natural to transition to Svelte right after a discussion on fundamental web features and APIs. That is to say, Svelte truly feels more like an extension of the web platform than a replacement. All the Web APIs and the DOM APIs are the same. There is no need to learn about replacements for the <a>
tag and whatnot. Background knowledge just transfers so seamlessly.
The language design of Svelte empowers beginners to prove to themselves that there is little to no magic behind UI frameworks; that everything is just built on top of the core Web APIs; and that everything is just plain old HTML, CSS, and JavaScript at the end of the day. The mental model perfectly bridges the gap between the world of components and the realities of the web platform.
SvelteKit: The Ultimate SSR, CSR, and SSG Solution
Before we proceed, let's begin by defining some terms:
-
Server-Side Rendering (SSR)
- The web server dynamically generates new (templated) HTML on-the-fly based on certain variables from the server, the database, the request, etc.
-
Client-Side Rendering (CSR)
- The web server sends an empty HTML app shell to the browser.
- The browser executes the JavaScript to populate the UI.4
- This usually implies extra round-trips to the server to fetch application data (typically in JSON).
-
Static Site Generation (SSG)
- The build system precompiles5 the HTML ahead of time.
- The web server thus only hosts static assets (e.g., HTML, CSS, and JS).
For many decades, the Web went back and forth between these three strategies—one trend after the next but never settling. To be fair, each has their own merits and trade-offs (as with most things in engineering).
However, what's most impressive about the Svelte ecosystem is its ability to seamlessly mix and match these strategies. This is where SvelteKit enters the picture.
Powered by Vite, SvelteKit is an officially maintained meta-framework5 that provides a full routing solution (client-side and server-side) on top of the Svelte framework proper. It combines the best of all worlds by empowering the developer to granularly choose which routes to render on the server (SSR), which routes to hydrate only on the client (CSR), and which routes to prerender as static HTML (SSG).
This seemingly zero-cost2 flexibility is a godsend for pedants such as myself who take pride in squeezing every last byte of efficiency in the network.6 This is a far cry from the olden days when we were often forced to commit to only one strategy per application—lest we "eject" from prescribed project structures and implement the magic ourselves.
For example, consider a PHP app or an Express app with EJS templating just for the sake of SSR. Also consider the trendy React apps from the pre-2016 era just for the sake of CSR. Or pehaps consider a Gatsby setup just for the sake of SSG.
In all cases, we often required that these be separate applications. Back then, we just didn't have a holistic solution/framework that streamlines the notion of hybrid rendering strategies at the granularity level of routes. Now we do.7
In other words, SvelteKit empowers us to ask not whether we should commit to SSR exclusive-or CSR exclusive-or SSG, but to instead ask which specific routes we can use SSR inclusive-or CSR inclusive-or SSG on. It is a truly liberating experience to have everything just work™ out of the box.
Accessibility as a First-Class Citizen
One feature that is especially admirable about Svelte is its treatment of accessibility as a first-class citizen. The Svelte compiler goes as far as to enforce accessibility best practices and warn against the anti-patterns.
The interminable yellow squiggly lines may seem cumbersome at first, but one must always keep in mind that disabling them misses the entire point!
Some posit that this is one of Svelte's most annoying features. I personally disagree. As someone who is not so well versed in the intricacies of accessibility, I greatly appreciate the automated reminders to get off my high horse of privilege and immerse myself into the viewpoint of disabled folks.
However, I must admit that event delegation patterns in Svelte are a little cumbersome. Event delegation usually involves attaching event listeners to non-visible parent containers, which the compiler flags as accessibility warnings.
Indeed, it is rather strange to attach a click
listener on seemingly arbitrary <div>
tags—even stranger when they're nested! Typically, these warnings are resolved by applying the correct ARIA roles and attaching the corresponding keydown
listeners. In these rare false positive cases, however, I am not totally against disabling the particular warning altogether.
Small Ecosystem
Yes, the Svelte ecosystem is relatively small especially when compared to that of React. At face value, this is a bad look on the ecosystem. One may even dare to say: "Svelte is dead!" But I believe there is a more satisfying reason behind the small ecosystem—there is just no need for a big one!
Recall that the Svelte language aims to be an extension of the web platform. As such, much of the framework internals use and accept (as arguments) the standard objects from Web APIs that are already built into browsers.
Consider for example fetch
, Request
, Response
, FormData
, Node
, CustomEvent
, etc. When most of the essentials are already built into browsers, it is rarely the case that a Svelte developer reaches out for libraries.
Easy Integration with Web-First Libraries
This is especially true when integrating with established third-party libraries (that were built for the Web rather than for a particular framework). One example that I can cite from experience is the Chart.js library, which works with plain old HTMLCanvasElement
objects. In Svelte, a simple bind:this
is sufficient to set things in motion without much ceremony.8 Of course, one may also opt to use wrapper libraries9 for convenience.
Nevertheless, the central point is that in the Svelte world, wrapper libraries are mainly for convenience. They are sufficient to get the job done, but not necessary as it is in other frameworks.10 It's rare for a third-party library to require some additional svelte-adapter-*
package just to cleanly interface with Svelte's reactivity.
No Need for State Management Libraries
Speaking of Svelte's reactivity, one more thing that is also worth mentioning is the fact that state management libraries are unheard of in the Svelte world. This is not because the "Svelte ecosystem is dead". Rather, to reiterate the point, the Svelte ecosystem does not need one!
The built-in stores and reactive variables cover most (if not all) of the use cases. In fact, the reactive primitives and the logic blocks are so powerful together that they can elegantly express component-level state machines—zero third-party libraries required!
On the off chance that these reactive primitives aren't sufficient, then perhaps it is a sign to reconsider the architecture altogether. There is always a better way to do things...
Fine-Grained Reactivity (Sometimes!)
Thanks to the compiler, Svelte boasts a "magical[ly] disappearing UI framework" where reactivity and state dependencies are resolved and bound at compile-time. For most primitive variables (e.g., strings, numbers, Booleans, etc.), fine-grained reactivity is sufficient and works great out of the box. In fact, it is exactly this fine-grained reactivity that enables Svelte to consistently outperform all of the UI frameworks except a few. More on this later.
At the component level, the unit of reactivity is the variable while the catalyst for reactivity is reassignment. Svelte will even prevent a DOM re-render if the newly assigned state is strictly equal to its previous value. Note that for objects and arrays, this check is done recursively to reach that extra mile in fine-grained reactivity.
Despite these guard conditions, however, an unfortunately common mistake with non-primitive variables (e.g., objects) introduces a subtle pessimization on reactive side effects (which execute before the re-render guard).
Consider the example below where we have a data
object with two fields: count
and other
. In the example, we will only modify data.count
whenever we click on a <button>
. Meanwhile, the data.other
field will always remain untouched.
<script>
// Recall that _only_ the `count` will be modified.
let data = { count: 0, other: null };
const increment = () => ++data.count;
// Intuitively, this side effect should run per increment.
$: console.log('updated count', data.count);
// With that said, will this side effect ever run?
$: console.log('updated other', data.other);
</script>
<!-- Increment `data.count` per click. -->
<button on:click={increment}>+</button>
Surprisingly, the side effect with data.other
will actually run! This goes back to the fact that variables are the units of reactivity while reassignment is the catalyst for reactivity. Since data.count
is inside the data
variable (as a whole), then all dependents on the data
object will also be notified of the change! The data.other
side effect executes simply because it just happens to implicitly reference the data
variable (as a whole).
The fix is frustratingly simple: normalize the primitives!
<script>
// Same old `data` here.
let data = { count: 0, other: null };
// Now we "normalize" the object into its primitives.
// Alternatively, we could have just separated the variables
// to begin with rather than putting them in an object.
$: ({ count, other } = data);
// Same click handler.
const increment = () => ++count;
// Now only this side effect will run.
$: console.log('updated count', count);
// This is unreachable code because we hold no references
// to `data` nor to the normalized `count` variable.
$: console.log('updated other', other);
</script>
<!-- Increment `count` per click. -->
<button on:click={increment}>+</button>
Kevin Bridges (@kevinast) goes into great detail about this behavior in his article on exploring Svelte's reactivity. The main point is that Svelte lets us "fine tune the reactivity"—for better or worse...
Responsive Svelte (exploring Svelte's reactivity)
Kevin Bridges ・ Jul 17 '20
There are more examples out there where Svelte is not as fine-grained as I first thought. To cite another example, the {#each}
block actually diffs by the provided key expression when the backing array gets modified. This should be an incredibly fast operation, but I was nevertheless disappointed to find out that there was no fancy compile-time machinery happening behind the scenes.
Returning to the framework benchmarks, perhaps it is for this reason why SolidJS consistently outperforms Svelte. Simply put, when it comes to non-primitive variables, SolidJS stores (which are essentially just recursive signals with fine-grained mutation helpers) are more fine-grained than that of Svelte.
Now if there was somehow a way to integrate (at compile-time!) the fine-grained reactivity of SolidJS signals with the expressiveness of the Svelte language, then that would be the most unstoppable framework ever! But alas, we do not live in a perfect world... yet!
UPDATE: On 20 September 2023, the Svelte team announced runes for Svelte 5. The signal-based rewrite of the framework internals implements finer-grained SolidJS-like reactivity at compile-time! Perhaps the perfect world is not so far after all... 🎉
The Exotically Magical Dollar
If there is one part of Svelte that is worth nitpicking, it is certainly the bold decision to change the semantics of a seldom-used JavaScript feature: labels. The $
syntax is simultaneously the most elegantly brilliant and also the most exotically magical feature of the Svelte language.
In the beginning of this review, I emphasized that Svelte prides itself in proving that there is no magic behind UI frameworks. This is the only exception.
As a JavaScript purist, it initially horrified me to see the $
everywhere. I could forgive (and even applaud) the $:
label syntax, but the store prefix (i.e., $store
) was where I drew the line.
In the beginning, I frequently found myself forgetting to prefix my stores with $
before accessing its inner value. Admittedly, this was a rather frustrating experience, especially considering the fact that it felt too magical for me. I knew that it desugared into the more verbose subscribe
pattern, but my inner purist self could not help but feel icky about the non-standard semantics. I eventually grew to embrace the syntax, but the journey was nevertheless jarring.
Whenever I teach Svelte to JavaScript beginners, I always made sure to emphasize that the $
syntax is a non-standard Svelte-specific feature. As a mentor, the obligatory disclaimer that prefaces all of the lectures eventually does get old. But alas, the incessant reminders are necessary—lest we fall into the trap of becoming Svelte developers (exclusively) rather than JavaScript developers.11
A Masterclass on Empowerment
For all of my major projects, Svelte has become my go-to framework. In all of my years of experience in full-stack development, no other framework comes this close to perfection (for now!).
- Onboarding has a low barrier to entry (i.e., HTML, CSS, and JS are the only prerequisites).
- Outstanding documentation and interactive playgrounds.
- Minimal framework magic (aside from the aforementioned
$
syntax). - Minimal performance overhead (aside from the aforementioned "not-granular-enough" footguns) if any.
- Beautifully intuitive syntax and semantics behind the template language.
- Keeps the spirit of zero-cost2 abstractions (or otherwise minimal overhead) alive.
- Maximal route-level flexibility in rendering strategies (with SvelteKit).
- Streamlined developer workflow and plugin system (thanks to Vite).
All of these reasons are why I never hesitate to advocate for Svelte at my department. I have personally led several successful Svelte projects in the past semester alone. Many of these projects, in fact, involved onboarding complete beginners and experienced developers alike. Regardless of the team composition, Svelte never failed to produce successful projects. The same is true even for other teams that I have offered technical consultation on.12
The astute reader may notice a common theme in this review: empowerment. Svelte and its zero-cost2 philosophy empower teams to move fast with ease and confidence.
Because Svelte does not require a degree in rocket science to understand, it also empowers individuals to believe that they are capable of building full-stack applications; that front-end development can be intuitive; that back-end development doesn't have to be so scary; and that they are more qualified than they initially thought.13
This is why Svelte is a masterclass on empowerment.
Needless to say, I am a huge fan of the Svelte team's work. I would like to thank them for empowering developers, keeping the spirit of innovation alive, and pushing the boundaries of full-stack web development.
-
Aside from the necessities, of course! Namely: hydration code, client-side routing, and effect scheduling. Note that these are actually optional if one decides to fully prerender their pages (i.e., static site generation). ↩
-
I mean "zero-cost" in a C++ sense that framework features are compiled and packaged into the final bundle if and only if they are actually used. Furthermore, the features that are compiled and packaged cannot possibly be written by hand any better. ↩
-
Yes, Svelte is a language. ↩
-
In Svelte parlance, this is called prerendering. ↩
-
Perhaps not exactly zero-cost in the sense that SvelteKit's SSR capabilities require (the overhead of) a Node.js-compatible JavaScript runtime rather than an external web server base layer written in Rust or Go for example. There are ways to work around this, of course, but that is beyond the scope of this article. ↩
-
Note that nowadays, SvelteKit is not the only framework that supports this level of flexibility. But then again, that is beyond the scope of this article. ↩
-
Yes, I'm aware that this is also possible in React with
useRef
, but that is not our topic for today. ↩ -
The
svelte-chartjs
library seems to be a popular option among Svelte developers. ↩ -
By "necessary", I mean that it is otherwise unwieldly to write the integration ourselves. That is, it is "unwieldly" in the sense that we have to consider so many footguns just to get it right: two-way data bindings, re-renders, caching, performance, etc. ↩
-
Such is the trap that many beginners fall into when they eagerly jump into React before learning the fundamentals of JavaScript first. ↩
-
Some teams built CSR Svelte apps on Electron. Others went for the traditional hybrid SSR and CSR route. On some of my projects, SvelteKit has become my go-to solution for SSG. ↩
-
In fact, one of my friends even went from zero full-stack knowledge to AWS-certified cloud practitioner in three months after recommending Svelte to them! ↩