Why Svelte won’t kill React

Is status quo to blame for that? Or is React simply better?

Kit Isaev
Kit Isaev
Dec 9, 2019 · 14 min read

When I just started reading Svelte docs I found it quite inspiring and was going to write a eulogy about it on Medium. After reading a couple of articles from the official blog and from the community I realised that this is not going to happen, because I noticed some signs of a common rhetoric in the JavaScript world — a rhetoric that upsets me a lot.

Hey, remember that problem that the powerful minds of the humanity have been trying to solve for 30 years? I’ve just found a universal solution! Why didn’t it conquer the world yet? Should be obvious. Facebook’s marketing team is plotting against us.

In my opinion it is okay to say your tool is revolutionary compared to existing ones. And it is hard to be fully unbiased about your own creation, I get it. Here is a positive example — I think Vue does a really good job comparing itself to other solutions out there. Yes, there are some doubtable statements I don’t agree with, but they are communicating a constructive message:

We have this approach, here are some other existing approaches. We believe ours is better, here is why. And here are some common counter arguments.

The official Svelte blog, on the contrary, ends up mind tricking the reader by showing only one side of the coin, sometimes through upfront false statements about web technologies and other libs (I will be mostly referring to React simply because I know it better). So in today’s article I will be primarily roasting Svelte just to balance it out. That being said, I still think there’s a brilliant idea behind it and I’ll tell you why at the end of the article 😊

imgflip.com

What is Svelte?

App.svelte

<script>
import Thing from './Thing.svelte';
let things = [
{ id: 1, color: '#0d0887' },
{ id: 2, color: '#6a00a8' },
{ id: 3, color: '#b12a90' },
{ id: 4, color: '#e16462' },
{ id: 5, color: '#fca636' }
];
function handleClick() {
things = things.slice(1);
}
</script>
<button on:click={handleClick}>
Remove first thing
</button>
{#each things as thing}
<Thing color={thing.color}/>
{/each}

Thing.svelte

<script>
export let color;
</script>
<p>
<span style="background-color: {color}">current</span>
</p>
<style>
span {
display: inline-block;
padding: 0.2em 0.5em;
margin: 0 0.2em 0.2em 0;
width: 4em;
text-align: center;
border-radius: 0.2em;
color: white;
}
</style>

An equivalent React component:

import React, {useState} from 'react'
import styled from 'styled-components';
const things = [
{ id: 1, color: '#0d0887' },
{ id: 2, color: '#6a00a8' },
{ id: 3, color: '#b12a90' },
{ id: 4, color: '#e16462' },
{ id: 5, color: '#fca636' }
];
const Block = styled.span`
display: inline-block;
padding: 0.2em 0.5em;
margin: 0 0.2em 0.2em 0;
width: 4em;
text-align: center;
border-radius: 0.2em;
color: white;
background-color: ${props => props.backgroundColor}
`;
const Thing = ({color}) => {
return (
<p>
<Block backgroundColor={color} />
</p>
);
}
export const App = () => {
const [things, setThings] = useState(things);
const removeFirstThing = () => setThings(things.slice(1))
return (
<>
<button onClick={removeFirstThing} />
{things.map(thing =>
<Thing key={thing.key} color={thing.color} />
}
</>
);
}

Svelte is not a framework — it is a language

My previous article covers various approaches to solving this problem in React using the means of JavaScript. Svelte takes advantage from its position as compiler to make reactivity a language feature². There are two new language constructs in Svelte that serve this purpose.

  • $: operator before a clause makes this clause reactive, i.e. it will be re-executed each time some of the variables it reads from updates. A statement can be an assignment (aka “dependent” or “derived” variable), or a code block or a call (aka “effect”). This is somewhat similar to MobX approach but built into language.
  • $ operator creates a subscription to a store (state container) that is automatically cancelled when the component is unmounted

Svelte’s reactivity concept allows using regular JS variables as state — no need for state container. But does it really improve the DX?

reddit.com

Svelte’s Reactivity

The original promise of React was that you could re-render your entire app on every single state change without worrying about performance. In practice, I don’t think that’s turned out to be accurate. If it was, there’d be no need for optimizations like shouldComponentUpdate (which is a way of telling React when it can safely skip a component) — Rich Harris, maintainer of Svelte³

The real problem is that programmers have spent far too much time worrying about efficiency in the wrong places and at the wrong times; premature optimization is the root of all evil (or at least most of it) in programming. — Donald Knuth, American computer scientist⁴

First of all, let’s make it clear. Even if you don’t have any single shouldComponentUpdate in your code, React does not re-render your entire app on every single state change. It is a very simple thing to check — all you need to do is add a console.log call to the root component of your app.

In this particular case, App will not be re-rendered unless isAuthorized state changes. No change to any of the child components will cause the App component to re-render. Components are only re-rendered if their own state changes, or when triggered by React Context, or during parent component re-render.

The latter case creates space for so called wasted renders — a situation when it is known in advance that parent re-render won’t cause any change in child’s DOM hierarchy, but the child is still re-rendered. This happens when child props are unchanged or when this particular kind of change isn’t supposed to affect what’s visible on the screen. To avoid wasted renders you can define shouldComponentUpdate (or use React.memo as a more modern functional alternative).

Optimizations must be exceptional, not default

This is a very dangerous practice as each optimization means making assumptions. If you are compressing an image you make an assumption that some payload can be cut out without seriously affecting the quality, if you are adding a cache to your backend you assume that the API will return same results. A correct assumption allows you to spare resources. A false assumption introduces a bug in your app. That’s why optimizations should be done consciously.

Svelte chooses a reverse approach. It will not re-run your component’s code on update unless explicitly told to do so, using the $: operator. I don’t want to spend dozens of hours searching for a place where I forgot to add one and trying to figure out why my app is not working — so that my users could enjoy a 20ms faster re-render. If there is a heavy component once in a while, I will optimize it, but it is an extremely rare occasion. It would be pointless to revolve my DX around it.

Svelte’s optimizations are not optimal

const shouldUpdate = (prevArr, nextArr) => {
if (prevArr.length !== nextArr.length) return true;
return nextArr.some((item, index) => item.id !== prevArr[index].id)
}

There is no way to specify custom reaction comparators in Svelte, it will fall back to this to compare arrays:

export function safe_not_equal(a, b) { 
return a != a ? b == b : a !== b
|| ((a && typeof a === 'object') || typeof a === 'function');
}

I understand that I could use some third party memoization tool on top of the Svelte’s comparator, but my point here is — there is no magic pill, optimizations “out of the box” often turn out to have limitations.

Inexpressive state updates

…the name of the updated variable must appear on the left hand side of the assignment.

Svelte magically adds a call to internal runtime invalidate function that triggers reaction. This can bring up some crazy patterns.

const foo = obj.foo;
foo.bar = 'baz';
obj = obj; // If you don't do this, update will not happen

Updating an array using push or other mutating methods also doesn’t automatically trigger a component update. So you have to use array or object spread:

arr = [...arr, newItem];
obj = {...obj, updatedValue: newValue};

Basically same as in React, except that in React you make a function call and pass updated state to it, whereas in Svelte you have an illusion that you are working with regular mutable variables. Which kind of reduces the whole point of this magic to “hey look how cool, Svelte is a compiler”.

Virtual DOM

Virtual DOM is valuable because it allows you to build apps without thinking about state transitions, with performance that is generally good enough — Rich Harris, maintainer of Svelte⁶

Almost every article in the Svelte blog claims that virtual DOM is an unnecessary overhead, and quite a high one, that can be easily replaced with pre-generated DOM updaters at no cost. But is this statement correct? Partially.

quickmeme.com

Does virtual DOM add overhead?

But is overhead always bad? I believe no — otherwise Svelte maintainers would have to write their compiler in Rust or C, because garbage collector is a single biggest overhead of JavaScript. I guess when making decisions about the stack of the compiler, they made a tradeoff — how high the overhead is vs the benefits the community gets in exchange. In this case, the overhead is relatively low — you don’t have a compiler constantly running on your device, you just run it from time to time, there is relatively little computation involved and a few seconds don’t make a big impact on UX. On the other hand, because Svelte is based on JavaScript and targets JavaScript as execution environment, having the tool written in TS/JS provides relatively huge benefits to the DX— everyone who is interested in the tool — and thus might want to contribute or might need to study the compiler sources — is likely to know JavaScript.

So overhead is always a tradeoff. Is it worth the cost in case of the virtual DOM?

The cost of virtual DOM

The first question is answered by Rich Harris himself:

We’re shipping too much code to our users. Like a lot of front end developers, I’ve been in denial about that fact, thinking that it was fine to serve 100kb of JavaScript on page load — just use one less .jpg!

But then he makes a note:

100kb of .js isn’t equivalent to 100kb of .jpg. It’s not just the network time that’ll kill your app’s startup performance, but the time spent parsing and evaluating your script, during which time the browser becomes completely unresponsive.⁷

Sounds serious, let’s do some measurements using Audit tool of Google Chrome. Luckily we have this possibility thanks to realworld.io:

React-redux:

Svelte:

The difference is 0,15 seconds — which means it is negligible.

But what about benchmarks? Benchmarks that Svelte blog refers to show that it takes React 430.7ms to swipe 1000 rows, whereas Svelte can do this in 51.8ms.

But this metric is not reliable because this particular operation is a weak spot of React due to reconciliation assumptions made by React — this scenario is very rare in real world apps, and same benchmarks show that the difference between React and Svelte in almost all other cases is negligible as well.

Svelte an React-redux on hooks comparation

And it’s time that we finally realise that those benchmarks should be taken with a grain of salt. We have windowing and virtualization, and rendering 1000 rows at a time is a bad idea anyway. Seriously, did you ever do it?

tenor.com

But Svelte maintainers claim vDOM is completely unnecessary — why waste any resources then?

Killer feature of vDOM

React code:

const UnorderedList = ({children}) => (
<ul>
{
children.map((child, i) => <li key={i}>{child}</li>
}
</ul>
)
const App = () => (
<UnorderedList>
<a href="http://example.com">Example</a>
<span>Example</span>
Text
</UnorderedList>
);

This is a very simple task for React and literally impossible for Svelte. Because templates are not Turing-complete, and if they were, they would require vDOM. It might seem like a small thing but for me it is more than a valid reason to add an extra of 0.15–0.25 seconds to my app’s time-to-interactive. This is exactly what we need the vDOM for — we might not need it for reactive state updates, conditional rendering or list rendering, but as long as we have it we can treat our component hierarchy as fully dynamic and controllable object. You cannot code a serious fully declarative app without this feature.

Temporary limitations (could be fixed in the future)

No TypeScript support

Immaturity

Consider interoperability. Want to npm install cool-calendar-widget and use it in your app? Previously, you could only do that if you were already using (a correct version of) the framework that the widget was designed for – if cool-calendar-widget was built in React and you're using Angular then, well, hard cheese. But if the widget author used Svelte, apps that use it can be built using whatever technology you like. — Rich Harris, maintainer of Svelte⁷

I already have whatever tools I can imagine for React — a dozen of GraphQL clients, over 30 form state managers, hundreds of DateTime inputs.

NPM search for “svelte”
NPM search for “react”

This would have been a killer feature back in 2013, but now it doesn’t matter anymore.

Bright future?

To be honest, I don’t believe Svelte in its current form can defeat React and conquer the world. It would be cool though to have a framework that does not add any specific limitations but 100% tree shakes all of the unused parts. And produces some build-time hints about its proper execution that could be used in runtime.

A note on readability

It’s unusual for the difference to be quite so obvious — in my experience, a React component is typically around 40% larger than its Svelte equivalent — Rich Harris, maintainer of Svelte⁸.

youtube.com

You only write each piece of code once and read many times. I know that it is the matter of taste and a debatable thing, I find JSX and regular javascript flow operators a lot more readable than any sort of {#blocks} and directives. I used to be a big fan of Vue before the peak of its popularity. Then at some moment I just stumbled upon limitations and inexpressiveness of templates and started to use JSX everywhere — and because JSX was not a typical thing for Vue I switched to React over time. I don’t want to make a step back.


Thanks for reading! 😍

I hope you enjoyed this article. If you have notes, want to discuss or debate — you are wholeheartedly welcome in the comments!


JavaScript in Plain English

Learn the web's most important programming language.

Kit Isaev

Written by

Kit Isaev

Full stack web developer (javascript)

JavaScript in Plain English

Learn the web's most important programming language.

Welcome to a place where words matter. On Medium, smart voices and original ideas take center stage - with no ads in sight. Watch
Follow all the topics you care about, and we’ll deliver the best stories for you to your homepage and inbox. Explore
Get unlimited access to the best stories on Medium — and support writers while you’re at it. Just $5/month. Upgrade