The sidebar component is my first project using svelte. This post contains a collection of thoughts on svelte. The focus is not on describing svelte’s concepts or syntax. Svelte.dev does an exemplary job at that. My opinions are mostly based on the comparison with react. I’ve dabbled around with the original angularjs, and played for fun with vue two years ago, but my habits built up in four years of using react professionally.
Setup experience & 1st code
It’s easy enough to bootstrap a svelte project using degit. Where create-react-app is a bloated mess full of bandwagons of hidden configuration, the svelte starter is a slim, no-frills project. How is it that react still doesn’t have a good onboarding experience? I’ve never used angular’s CLI, but I can sing the praises of vue CLI. Compared to that, having exactly one preset for svelte projects is a bit skimpy.
That’s more than made up for by svelte’s tutorial, which moves in small, easily understandable, interactive steps: You can go there right now and skim over it in an hour. Small code pieces go hand-in-hand with short explanations of the concepts. The approachability hides the surprising amount of depth covered. No question: Readers of the tutorial with previous knowledge of a modern javascript framework and web development will be able to build applications right after. Good luck researching the best solution to manage data in your react project in that time. Svelte’s onboarding curve is even less steep than I rember vue’s, whose docs I adored. For those who want to play around to get an understanding, the REPL is a good start. It showcases a preview next to the surprisingly readable results of the svelte compiler – which is far more decipherable than the output of a typical babel plus webpack setup.
Back to the bootstrapped degit project: it works, it’s small, and therefore straight forward to start with. The fact that the starter project uses rollup instead of webpack is a source of joy. During the first few hours I found myself copy-pasting from the different cases covered in the tutorial a lot. As the tutorial’s examples are well-chosen, they apply to a wide range of situations new starters will likely find themselves in.
Quickly, a warm feeling settles in: svelte feels complete and polished.
Developing a javascript web app with svelte gets close to the effortlessness of developing a static website. That’s for one because components are look-alikes to an HTML file with inline style and script. Secondly, svelte can be used without third party libraries. It covers basics like working with (scoped) styles, provides effective tools to deal with async requests, application-wide data, and even luxury features like transitions and animations.
Syntax oddities
Template syntax
Svelte comes with its own template syntax, which is closer to angular’s and vue’s than jsx. It makes for a HTML-alike feel when authoring component’s markup. Designing a svelte specific template syntax has enough trade-offs that I’m wondering about the motivation not to re-use battle-proven syntax already familiar to javascript developers.
- It’s required to install extensions in the code editor to get proper syntax highlighting.
- The eslint and prettier plugins are still rough around the edges.
- Devs picking up svelte have to put their knowledge about other template syntaxes on the back burner and instead learn the new one.
The same arguments could be made about vue’s or angular’s template syntax – but they were first. It seems wasteful to reinvent the wheel, given that developers have pre-existing knowledge .
To be clear: None of the above is hard. But the svelte authors made some strange choices when it comes to syntax: Why is an if
condition beginning with a different control character than the condition’s end?
{#if condition === true}
<p>it's true!</p>
{/if}
The first few days, I typed {#endif}
more times than I care to count. Not a major turnoff, but most people onboarding will experience unnecessary friction when starting due to small choices like this.
Props
Svelte’s syntax can be eccentric not only in templates. See how a component Button
defines that it expects the property size
:
<!-- Button.svelte -->
<script>
export let size
</script>
It feels strange that the component receiving a property exports it as a mutable let
. Using let
let’s me shudder a little bit every time. For me, using let
is like consciously adding a code smell, it’s dirty. Feelings are no strong argument against the syntax. But I’m probably not the only one who finds this counterintuitive – after all we javascript developers spend the last years embracing typing (flow, typescript), or at least the concepts of immutability (e.g. with the flux pattern, immutable.js). Whenever I stumble across a let
during code review I will triple-read the function. Usually there’s a more readable and preferable versions without let
. Re-assignments can trigger all sorts of subtle bugs!
Now let’s say I want to add the following variables as props to Button
:
const size = 'large'
const primary = true
const onClick = () => alert('you clicked me!')
In react individual props can passed inline without destructing.
<Button size primary onClick>
My button
</Button>
In svelte, passing props is more repetitive and therefore, once a certain amount of props is being used, hard to read:
<Button size={size} primary={primary} onClick={onClick}>
My button
</Button>
This is not entirely fair - it’s possible to spread an object of props. It just requires the component to assemble and destruct an object of props in curly braces – not quite as simple, as the react example:
<button {... size, primary, onClick}>My button</button>
Yes, I’m nit-picking here - it’s just that in other areas svelte minimizes the syntax overhead better. Like for example when attaching a one-time event listener. Similarly to vue it’s boiled to the minimal possible syntax:
<button on:click|once="{handleClick}">
<slot></slot>
</button>
Let’s move on to another one of svelte’s strong cases.
Styling
It’s comfortable to write automatically scoped styling next to its markup. Vue paved the way here with scoped CSS.
On top of taking care of scoping, svelte comes with elegant syntax to bind classes conditionally.
Class binding
Here’s how to bind classes from props with svelte sans third party libraries:
<script>
export let size // one of: 'large', 'small'
export let primary // one of: true, false
</script>
<style>
.size {
...;
}
.primary {
...;
}
</style>
<button class:size class:primary="{primary}">
<slot></slot>
<button></button>
</button>
The pure react equivalent would be much harder to read:
export const Button = (size, primary) => {
return (
<button className={`${size ? size : ''} ${primary ? 'primary' : ''}`}>
{children}
<button>
)
}
This syntax pain enabled the rise of small helpers like classnames
:
import classnames from 'classnames'
export const Button = (size, primary) => {
return (
<button className={classnames(size, { primary })}>
{children}
<button>
)
}
It’s not a coincidence that CSS-in-javascript gained massive traction.
The argumentation from the react crowd is that react gives freedom to the developer instead of enforcing specific solutions. Every single developer using react has to decide on a way to handle styling and overcome the friction of setting it up. I suspect that facebook’s upcoming CSS-in-javascript solution will soon be seen as standard way to style react applications, proving the point that developers expect their main frontend framework – react is a library, I omit some precision for readability – to provide a sensible default.
Unused style detection
But svelte’s most unique (compiler enabled) feature is its automatic detection of unused styles. Having experienced this comfort, my eyes glaze over with irritation whenever I during PR review once again find myself comparing styles and a component’s jsx in a GitHub div because an existing component was refactored and I need to ensure that all remaining styles are being used.
How to compose pages from components?
What I’m missing, is a pattern to compose pages with reusable components. When using a component, I often need to define small style variations that are unique to the specific usage. This might be setting a margin or display property, a unique hover color, or a special width.
<!-- LoginPage.svelte -->
<script>
import Button from './Button.svelte'
...
</script>
<main>
<h1>Login</h1>
...
<!-- button is supposed to be full width -->
<button {...props}>Login</button>
</main>
vs.
<!-- SearchBar.svelte -->
<script>
import Button from './Button.svelte'
...
</script>
<style>
/* darn, no way to target <Button/> */
</style>
<input bind:searchTerm></input>
<!-- button is supposed to be in line with search input -->
<Button {...props}>
Search
</Button>
How would I do that?
Foresee every different possible variation of Button
, and extend Button
with a bunch of optional props to allow to customize it to all of the possible versions? I’d never stop having to rewrite Button
, and end up with an unmaintainable component signature.
Or add a style
property to every reusable component and pass a value in every usage that requires customization? Probably.
It would be nice, could I attach a class to Button
and target it in the component that uses it. Though that could only work for components returning a single DOM node, which is not required by svelte.
Common use case examples
Reactivity & local state
Let’s get back to the criticism that svelte uses let
. It makes defining and updating state as breve as possible:
<script>
let clicks = 0
function onClick() {
clicks++
}
</script>
<button on:click="{onClick}">
<slot></slot>
</button>
No need to remember arbitrary setter syntax. I’d say: “beautiful!” – if there weren’t gotchas. To update e.g. arrays or objects, assignments can be necessary. The following won’t do the trick:
<script>
let clickTimes = []
function onClick() {
clickTimes.push(new Date())
// use assigment to make it work: clickTimes = clickTimes.push(new Date())
}
</script>
<button on:click="{onClick}">
<slot></slot>
</button>
Now, push
is a mutating function, and therefore a little bit conspicuous anyway. Its usage is easy to replace. But wouldn’t any javascript developer expect a push
on a let
to update the value?
Definitely! You see: me and let
, we’re not becoming friends!
So there is some magic going on when updating state. It’s not the only sprinkles of it. Svelte rightfully markets its easy to use reactive declarations / statements, where a component update causes a re-execution of a computation. To define reactive code, prepend it with $:
. That stuff is powered by labeled statements. Never heard of it? Me neither! But… it works.
Store & state management
Where component state in svelte have the simple syntax of variables in react’s render functions, svelte’s stores are as simple as react’s useState
hook. Without further introduction a copy-paste of svelte’s tutorial:
import { writable, derived } from 'svelte/store'
export const name = writable('world')
// `greeting` re-computes whenever `name` is updated
export const greeting = derived(name, ($name) => `Hello ${$name}!`)
The usage:
<script>
import { name, greeting } from './stores.js';
</script>
<!-- read the computed greeting from the store -->
<h1>{$greeting}</h1>
<!-- 😍read from and write to the store by binding `name` to the input -->
<input bind:value={$name}>
<!-- update `name` from anywhere with re-assignments like local state -->
<button on:click={() => $name += '!'}>
Add exclamation mark!
</button>
Couldn’t be simpler, right? Well, the devil is in the detail: $
, which with colon would signal a reactive declaration / statement, stands for “This is a store value. When you unmount the component svelte, please remove the subscription to the store to prevent memory leaks”. It’s easy to imagine confusing components. I guess using stores and reactive declarations in the same component is an obvious anti-pattern. No big loss, as the reactive computation can be moved to the store.
Async data fetching
No web app without network calls. Network calls mean hassle: showing a UI state during the loading, providing a fallback UI in error cases, and most importantly, the desired view state when the data are present. In react, handling network calls and connecting them to the UI states is often done with libraries. Suspense will ease the …suspense. Svelte again keeps it simple:
<script>
let promise = getDataFromAPI()
async function getDataFromApi() {...}
</script>
{#await promise}
<p>loading</p>
{:then userName}
<p>Your username: {userName}</p>
{:catch error}
<p>{error.message}</p>
{/await}
Transitions & Animations
How awesome it is, to drop in a one-liner and have working transitions or animations!? With svelte, it’s that simple:
<script>
import { fade, fly } from 'svelte/transition'
</script>
<button in:fade out:fly>
<slot><slot>
</button>
Let me admit, that the above is oversimplified. Not because it doesn’t work perfectly well, but because I’m leaving out that much more complex transitions and animations are in reach. Extension with self-written CSS or javascript transition, transition events, orchestrated animations with an animation directive - easy peasy!
Other features worth mentioning in passing
There’s many more features that can be compared to react and vue in some way or another. For example:
- Lifecycle hooks are same old, same old.
- Slots work comparable to vue (and react’s
children
prop). - Context looks exactly like react’s – but is not reactive. Because of that its main use-case is distributing static(ish) configuration throughout the application.
- The
use
directive is quite distinct, as the directives are element lifecycle functions instead of component-wide hooks as in react. - Event forwarding adds a clever touch to svelte.
- Handling user input luckily works similar to vue, with automatic binding to the component’s state. That’s so much more comfortable than calling a handler function, extracting the events
target.value
, and setting state withsetState()
. - Setting the documents head with
<svelte:head>
is a god-sent. Another dependency saved!
Performance
Bundle size
Let’s move to the topic of the impact of the framework on the amount of code shipped to users. There’s two reasons to care about that:
- Data transfer incurs monetary costs for users with non-flatrate data plans. Data transmission also uses energy, which influences the environmental impact of your application.
- There’s a strong correlation between amount of javascript code loaded and executed, and performance – for reference see 1 and 2. Just to recap:
- No javascript is faster than a lot of javascript. At the same time it’s not the case that more javascript is necessarily slower than less javascript. LinkedIn shared an example. It depends on the case-specific code.
- Building one big fat bundle for a SPA comes with slow initial page load and TTI time, and is generally discouraged. The benefit of a single fat bundle is that the user is done loading javascript after the inital page load. A high one-time cost might be acceptable to (repeating) users of some applications, especially if caching is handled well or service workers are in play. Splitting the bundle into chunks (e.g. by route) will reduce the inital page load and TTI time – good for conversion rates and applications with a lot of one-time visitors. On the other hand those users might experience further small load times when navigating the app due to the loading of more chunks during usage, decreasing the benefit of having a SPA application. The extremes approach on that end would be to not bundle at all, but to load every dependencies code separately, as promoted by snowpack.
- A SPA application with more business logic / features requires more code.
It’s not possible to know how relevant bundle size is and what the best bundling tradeoff for a project is without having context on the application’s use-cases and users.
After this detour, let’s get back to bundle size: It’s entirely possible to use svelte, a full-featured framework capable of delivering complex SPA applications with all of its developer comfort just to sprinkle some small interactivity into a regular homepage, and come out with a fraction of the amount of javascript one would ship, if one were to implement the dynamic behavior by writing a couple of lines of jQuery.
Traditionally using any library / framework like react or vue requires to ship the framework as a whole to the user, at considerable base-line fixed costs. At the same time, their code provides abstractions that allow to add more application code without linearly increasing the bundle size. Svelte behaves the opposite: The hello world
example is compiled to 3.029 bytes - it’s hard to get the code much leaner with a framework (preact
and hyperapp
allow for similar sizes). As components are compiled to pure javascript without a lot of shared helpers, there’s repetition though (see svelte discussions like 1, 2).
For supremely large applications, using react may result in a smaller bundle then when using svelte 1. My sidebar project was too contrived to get close to that point. When the bundle size of svelte applications realistically surpasses the size of other popular javascript libraries / frameworks, and whether that results in unfavorable performance, is an ongoing discussion in the svelte community. So far no one has raised actual problems - which either means that no one is yet using svelte for big applications, or that svelte shows good enough performance characteristics in those cases.
Rendering Performance
Other’s have benchmarked svelte in comparison to pure javascript and other javascript frameworks, with good enough results. This is insofar noteworthy, as in that svelte does not rely on a virtual DOM. The linked benchmarks mainly stress test rendering related activities which occur in every typical web application, and boot-up time. The abstraction of comparing the smallest common determinator naturally limits the meaningfulness of (the) benchmarks - it’s hard to know how a frameworks code would perform for your application without porting it.
Even if one were able to pick the fastest javascript framework when it comes to rendering, the UX would still depend more on whether one would use the framework correctly. At least with react – but probably all frameworks – it’s easy to accidentally cause countless expensive re-rendering cycles in actual usage by composing components badly. How data loading is handled has drastic impact as well given that network roundtrips are often more time intensive than the execution of some badly written javascript.
Most javascript frameworks are good enough when it comes to raw rendering performance, and svelte is no exception. Raw rendering performance is a weak signal in 2020 when arguing for or against a framework.
Ecosystem
Let’s not kid ourselves: There’s no ecosystem to speak of around svelte besides sapper, a next clone. Even sapper seems half-abandoned the past few months. That’s natural given that svelte is small, and there’s enough competition of other amazing javascript frameworks out there. As svelte comes complete out of the box, there’s rarely a need for a lot of svelte specific utilities though. Getting help can be a bit trickier, as the discord is mostly filled with developers starting with svelte. Not the best place for detailed conversation given its ephemeral nature.
Conclusion
Coming from react, svelte’s syntax is at times unnecessarily eccentric. But, once used to it, the hurdles of entry are uncharacteristically low for a modern javascript framework. Svelte is coming with single file components, and the co-location of javascript, CSS, and HTML template feels a lot like developing a static website with inlined javascript and CSS.
In addition to that, svelte comes with built-in solutions for use-cases developers will run into when working on a biggish SPA. Unlike react, there’s zero project setup choices to make when getting started on a web app. Svelte can afford to provide niceties and niche features. Most might not be used by everyone – and therefore they are rightfully not part of react, which is already too heavy as is – but thanks to the compiler, svelte’s functionality will only be part of the shipped code if used. It’s the opposite approach to concatenating a framework’s code with dozens of dependencies and then attempting to tree-shake dead paths out of the bundle. This craziness came up alongside the rise of javascript frameworks like the heavy but barebones react. With react it’s up to every developer to build a solid app fundament of documented, maintained libraries that provide modern, tree-shakable modules, whose code is not bloated, and whose APIs fit in.
Svelte is not yet as mature as the competition, with considerably lower adaption and without a real ecosystem. A few rough edges need to be ironed out, and svelte’s excellent documentation does not yet share best practices like vue’s (style guide). Maybe no one yet built applications large enough to have stumbled across patterns?
Behind the scenes, svelte takes the somewhat novel approach of compiling the application code and required framework code into plain javascript. Another technical choice diverging from players that have dominated the space across the last couple of years is that no virtual DOM is used. In practice, all of that comes with no noticeable downfall, but a few benefits:
- Application bundles can be super small. The size difference to authoring plain javascript yourself is negligible in those cases.
- Compiler warnings. We’ve got eslint, and typescript, and dev environment warnings for other frameworks - none of them are as deeply integrated as the svelte compiler. Stuff like detecting unused CSS without false positives or negatives feels like the sort of magic that should be standard in 2020.
- Svelte is future proof as it’s built on top of the assumption of a compiler. If web components ever reach mass adoption, svelte can change the default compile option to output web components, and that will be that.
Taken all together, svelte is comfortable for developers, while providing a good foundation to offer a great user experience. It’s nice to know that in svelte projects the DX won’t be as much at odds with the UX compared to a similar react project, where two handful of additional libraries are required to reach feature-parity with svelte.
Svelte is a language.
Some tinkering later, the statement that svelte is a language makes sense. Whether language or javascript framework, there’s plenty competition. I wish you the best of luck, svelte!