What Makes Svelte So Great – Features and Comparisons

Intro

At this point, many people who have worked with me at my current job know I’m an (unashamed) Svelte fanatic; I spread the word about it constantly, use it on both professional and side projects, and am not afraid to throw jabs at React for falling short of what Svelte delivers.

In order to help spread the word even further, as well have a place I can point people to when they ask me why I like it so much, I’m creating this post as a love-letter to Svelte, sorry, I meant to say carefully thought out list of reasons why I like Svelte (and I think you will too!).

Also, I worry it might feel like this post is constantly bashing React or might feel like a Svelte vs React showdown – while there is definitely some of that going on, I want to point out that:

  • I’m doing it out of a passion for web development and the hope that if we are honest about deficiencies, we can collectively work on addressing them. Competition is good, and React is making some good strides!
  • I still use React when it makes sense, both professionally and on hobby projects; it isn’t going anywhere anytime soon, and I’m OK with that.
  • Svelte is not a panacea – if you take poorly written React and re-write it in poorly written Svelte, it is still going to be poorly written. Garbage in, garbage out spaghetti code in, spaghetti code out 🍝

OK, now onto my fawning over careful analysis of what makes Svelte so great.

Performance

To get the ball rolling, I’m going to start with something objective, which is hard to argue against – Svelte performs faster and with less resources than React and many other frameworks. 🔥

This mostly comes down to how Svelte handles reactivity and DOM updates, which it has described as “surgical”. And this is without any hand-tuning or special code; no useMemo, useRef, or having to always be thinking about the render lifecycle – it just works.

If you don’t want to take my word for it, just check out these JS benchmarks. Right now, in December 2023, of the popular frameworks people are probably most familiar with, the rankings are generally (from best to worst): vanillajs -> solid -> svelte -> vue -> angular and react (pretty close together).

Simplicity

This is kind of jumping the gun, as this is the unifying theme of this post and how I wrap it up at the end as well, but if you walk away from this reading experience with only one takeaway about what makes Svelte so great, I think it should be that Svelte is simpler to use.

I’ll end this section with the same text I end this entire post with:

At the end of the day, I value simple, clean code that is easy to write, read, and maintain – for me personally, Svelte is the framework in which I find this the easiest to achieve, with the least amount of time and effort.

Bi-Directional Variable Bindings

Did you hear that sound? Its the sound of tears falling as this section is read by React devs who have to deal with form input listeners (hello formik users), callback hell, painful state refactoring, and everything else that comes with trying to pass values upwards in React and makes it such a chore.

Yes, you read that tile right – Svelte support bi-directional data bindings / variable bindings, out of the box. Once again, that is data flowing back up from the child to the parent, not just down.

To paint a picture of what that means and how it saves so much time, imagine you have a form input for a username, and you want to keep your local variable in sync with the input as the user types.

In React, if you don’t use a 3rd party library, you are going to have to write boilerplate to listen for the input value change and then update the variable yourself; React does not support bi-directional bindings:

import React, { useState } from "react";

export const AgeEditor = () => {
  const [userAge, setUserAge] = useState(50);

  return (
    <div>
      <input
        type="number"
        value={userAge}
        onChange={(evt) => {
          setUserAge(parseInt(evt.target.value));
        }}
      />
      <p>Your age is {userAge}</p>
    </div>
  );
};

In Svelte, you can just bind the variable directly:

<script>
  let userAge = 50;
</script>

<input type="number" bind:value={userAge} />
<p>Your age is {userAge}</p>

BUT – that’s not all! If you order within the next 5 minutes, you get bindings for so much more than just <input>s. You get them for ANY property export from a child component. This is huge, as in other frameworks like React, you would either have to pass a state updater as a prop to the child (e.g., a useState setter or your own callback) or use something like useContext (perhaps with a shared reducer) to establish shared state – all this to say that it is far less code in Svelte and much simpler:

Parent component:

<script>
    import AgeEditor from './AgeEditor.svelte';
    let userAge = 50;
    $: userAgeDoubled = userAge * 2;
</script>

<!-- 
We could have used `bind:userAge={userAge}`, but this is
even _more_ concise!
-->
<AgeEditor bind:userAge />

<p>Your age doubled = {userAgeDoubled}</p>

Child component (AgeEditor.svelte)

<script>
    export let userAge = 50;
</script>

<input type="number" bind:value={userAge} />
<p>Your age is {userAge}</p>

Finally, you can also use bi-directional bindings with entire components, through the bind:this directive.

State, State, State (Did I Mention State Yet?) and Effectful Code

I’m going to be very honest here – I really don’t care for React’s built-in state management solutions and how it treats reactivity in regards to state variables, and I think Svelte’s approach is FAR superior.

In Svelte, you don’t need to do anything special to make a variable “stateful” – it just is.

<script>
    let count = 0;
</script>

<button on:click={() => count++}>
    Count = {count}
</button>

In React (and some other frameworks), this does not work, because regular variables are re-computed every time the component re-renders. So count would get reset to 0 unless you wrap it in a special useState hook.

Here is the same code in React:

import React, { useState } from "react";

const App = () => {
  const [count, setCount] = useState(0);

  return (
    <button onClick={() => setCount(count + 1)}>Count = {count}</button>
  );
};

For a simple example like this, using useState might not look so bad. But once you have multiple pieces of state, and some pieces are derived from other pieces, and there needs to be side-effects that happen when certain pieces change, it starts to become a headache to manage. Especially because of the “recompute on re-render” piece of how React works also means that things like const myFunc = () => {} are also recomputed on component re-render, unless wrapped in useCallback.

All this to say that it is not uncommon to see a single component in React with a bunch of complex useState, useEffect, useCallback, and useMemo hooks, for not that much resulting UX. And these hooks tend to have many easy-to-miss idiosyncrasies that make it a little too easy to write bugs with.

There is a reason why there is a joke that you could rename useEffect to useFootGun

In comparison, again, local variables are stateful in Svelte without any sort of special syntax. And for derived variables or effectful code, it just requires using a reactive declaration or reactive statements, respectfully. There is no need for useMemo or useCallback entirely!

This is far from my best work in either framework, but here is a slapped together example that shows how much more boilerplate and complexity is generally required in React than in Svelte for a reactive UX. Notice how not only does the simplified state management result in less code, but I’m also able to take advantage of bi-directional binding and other shortcuts in Svelte.

Large example, so click to expand:

Click to show / hide React Version

React:

import React, { useState, useEffect, useCallback } from "react";

// Mock function, invalidates coupon because special holiday is over
const checkCoupon = () =>
  new Promise((res, rej) => setTimeout(() => res(false), 1000 * 2));

export const App = () => {
  const [salesTax, setSalesTax] = useState(6.5);
  const [basketItems, setBasketItems] = useState([]);
  const [couponActive, setCouponActive] = useState(false);
  const [subTotal, setSubTotal] = useState(0);
  const [total, setTotal] = useState(0);

  // If we don't use `useCallback`, this function is recreated on every re-render
  const addItem = useCallback(() => {
    setBasketItems((basket) => [...basket, { cost: Math.random() * 100 }]);
  }, [setBasketItems]);

  useEffect(() => {
    let preTaxSum = basketItems.reduce(
      (running, current) => running + current.cost,
      0,
    );
    preTaxSum = preTaxSum * (couponActive ? 0.8 : 1);
    // Note the extra indirection here ^ and below;
    // we can't just assign directly to `subTotal` and reuse for total
    setSubTotal(preTaxSum);
    setTotal(preTaxSum * (salesTax / 100) + preTaxSum);
  }, [couponActive, salesTax, basketItems, setTotal, setSubTotal]);

  useEffect(() => {
    // useEffect does not support making the outer
    // function async, so we have to do this.
    // Another hoook foot-gun 🙃
    const verifyCoupon = async () => {
      const verified = await checkCoupon();
      if (!verified) {
        setCouponActive(false);
        console.warn("Coupon is no longer valid");
      }
    };
    if (couponActive) {
      verifyCoupon();
    }
  }, [couponActive, setCouponActive, checkCoupon]);

  return (
    <>
      <label>
        Coupon?
        <input
          type="checkbox"
          checked={couponActive}
          onChange={(evt) => setCouponActive(evt.target.checked)}
        />
      </label>
      <label>
        Sales Tax ({salesTax.toFixed(2)}%)
        <input
          type="range"
          value={salesTax}
          min="1"
          max="50"
          step="0.5"
          onChange={(evt) => setSalesTax(parseFloat(evt.target.value))}
        />
      </label>
      <button type="button" onClick={addItem}>
        Add Item
      </button>
      <p>Item count = {basketItems.length}</p>
      <p>Subtotal = ${subTotal.toFixed(2)}</p>
      <p>Total = ${total.toFixed(2)}</p>
    </>
  );
};
Click to Show / Hide Svelte Version
<script>
  // Mock function, invalidates coupon because special holiday is over
  const checkCoupon = () =>
    new Promise((res, rej) => setTimeout(() => res(false), 1000 * 2));

  // Easy state variables!
  let salesTax = 6.5;
  let basketItems = [];
  let couponActive = false;
  let subTotal = 0;

  const addItem = () =>  basketItems = [...basketItems, { cost: Math.random() * 100 }];

  // Reactive / derived / side-effects
  $: {
     subTotal = basketItems.reduce(
      (running, current) => running + current.cost,
      0,
    );
    subTotal = subTotal * (couponActive ? 0.8 : 1);
  }
  $: total = subTotal * (salesTax / 100) + subTotal;
  $: (async() => {
    if (couponActive) {
      const verified = await checkCoupon();
      if (!verified) {
        couponActive = false;
        console.warn("Coupon is no longer valid");
      }
    }
  })();
</script>

<label>
  Coupon?
  <input
    type="checkbox"
    bind:checked={couponActive}
  />
</label>
<label>
  Sales Tax ({salesTax.toFixed(2)}%)
  <input
    type="range"
    bind:value={salesTax}
    min="1"
    max="50"
    step="0.5"
  />
</label>
<button type="button" on:click={addItem}>
  Add Item
</button>
<p>Item count = {basketItems.length}</p>
<p>Subtotal = ${subTotal.toFixed(2)}</p>
<p>Total = ${total.toFixed(2)}</p>

SFCs are SFC (so-frequently-cool) and Template Syntax vs JSX

For standard Svelte projects, each of your components are going to be built as a separate SFC (Single File Component).

These are composed of:

  • Any amount of regular HTML
  • Svelte templating code / syntax (not JSX)
  • <script> section (imports, exports, logic, assignments, etc.)
  • <style> section, for styling

It should be noted that you can omit any of the above and there are no constraints on how many root nodes a component can have.

For example, here is a valid Svelte SFC:

<script>
    let name = 'world';
</script>

<h1>Hello {name}!</h1>

<style>
    h1 {
        color: red;
    }
</style>

But, also so is this:

<p>Hello</p>

There are a few features of SFCs that are winners in my book:

  • Colocation! I find that having all three things – the logic, markup, and styling – all self-contained in one location, per-component, is a huge win for reducing complexity and keeping things clean.
  • <style> code is automatically scoped to just that component!
    • This means you can do something like p { font-size: 30px; } and it only changes the font-size of <p> tags within that file.
    • This is a game-changer if you are coming from React or vanilla HTML + CSS
    • For many projects, this can eliminate the need for things like SASS, CSS-in-JS, Tailwind, or other libraries that try to help with CSS complexity
  • A lot of restrictions that exist in JSX / React – such as only having one root node (requiring <Fragment> to wrap if necessary), and having class be a reserved keyword, thus requiring className="" – none of these exist in Svelte’s templating syntax
  • Valid HTML is valid Svelte code – you can just drop entire blocks of HTML into a Svelte SFC and it should work. The whole thing feels very natural and a shift back towards using the actual HTML standard more 💪

Finally, this might be a personal preference, but I prefer Svelte’s logic blocks to JSX’s use of pure JS, as I feel it results in a flatter structure that is more readable.

Shorthand Syntax Everywhere

A powerful feature of Svelte’s templating / markup language is that it has a lot of shorthand syntax throughout different areas. Some examples:

  • Using variable name directly, as binding
    • In JSX, you often see a lot of code like <Book author={author} />. In Svelte, if the variable name is the same as the prop, you can use a shortcut: <Book {author} />
    • This works for bindings too: bind:author instead of bind:author={author}
  • preventDefault and other DOM event modifiers
    • If you have done web development, you might be familiar with event.preventDefault(), to prevent the default event handler from running. Svelte has a shortcut to apply this, and other modifiers, to event listeners: on:eventname|modifiers={handler}
    • Example: <form on:submit|preventDefault={handleSubmit}>
  • CSS bindings
    • CSS variables / custom properties have become very powerful and useful in modern web development, and with Svelte, you can set them, inline with your markup, either on entire components with component style props (--my-css-var={val}), or with a style directive (style:--my-css-var={val})
    • Classes can be conditionally attached to DOM nodes, with class: bindings. E.g., <div class="expander" class:open> to conditionally combine .open with .expander, based on a local open variable.
  • And tons of other Element Directives, Component Directives, Special Tags, and more!

Batteries Included

One of the common concerns or complaints I hear when it comes to considering Svelte is:

But there are more libraries out there for React!

True… and yes, sometimes this is a legitimate concern, but this also brings me to one of my favorite things about Svelte, which is that so much stuff is provided out of the box for state management, reactive bindings, form logic, styling, etc., whereas with React you pretty much just get… JSX and hooks.

This isn’t even a comprehensive list, but here are some random freebies that are included with Svelte that I enjoy using:

Plus, if you use the standard Svelte tooling – such as SvelteKit – you’ll also get:

  • Hot module reloading (aka HMR)
  • Advanced rendering options (SSG, SSR, hybrid, etc.)
  • Routing system, optional eager pre-loading, etc.
  • Extensible settings / configuration (does not require “ejecting”, unlike CRA, for full control)

Incredible Documentation and Tooling

The Svelte docs are, hands-down, some of the best I have ever seen.

In my opinion, they strike the perfect balance between detail and brevity, between being low-level and high-level, and between being beginner-friendly but also not leaving out the info you need.

📄 I’ve written about the importance of balanced documentation before – see High Level Docs Matter

In addition, Svelte has guided tutorials, live examples, and a free REPL.

It’s Fun to Use, Simple, and You’ll Write Less Code with Less Bugs

I’m ending with this one, because if I put it first, I have a feeling it would turn people off from this post. Saying a framework is “fun” and “simple” is not exactly an objective statement, and honestly usually I get irked when I read something like “___ is magical”, but…. y’all… honestly Svelte IS magical 🎉

Coding with Svelte, above all other UX frameworks I’ve used, is the closest I’ve felt to having my keyboard be an immediate extension of my thoughts – I can go from an idea to functional prototype so fast that it feels like I have a mind-link with my computer (OK, maybe slight exaggeration, but you get the picture).

With other frameworks, like React, I find that I have to add in a step to translate my mental model into the frameworks model for doing things – e.g., I know how I want to make this UX interactive, but how do I get this working with React’s useEffect hook and its other idiosyncrasies?

🐍 If you are familiar with Python and a fan of how it tends to orient itself (with pythonic approaches, the Zen of Python) towards what many would call “natural” or “simpler” coding, than I think you will like what Svelte has to offer.

I would also add that, by allowing for less complicated code that is more readable and comprehensible, Svelte code (on average) likely results in fewer bugs. Or at least bugs that are easier to fix.

Nothing’s Perfect

It feels dishonest to only talk about the good things, so here are some things that are less than perfect when it comes to Svelte.

Reactivity in Svelte Requires a Mental Model Shift

In general, “thinking reactively” in Svelte will take some getting used to, and IMHO, will be the most challenging part of onboarding into the Svelte world for someone new to Svelte.

In particular, Reactive Statements, i.e. the $: {} blocks in a Svelte component, can be tricky and have some “gotchas”.

However, that being said, this one is really not so bad because A) it is nowhere near as bad as it is for useEffect with React (sorry React, but you know its true), and B) The new Runes feature in Svelte 5 should solve this (the gotchas, that is).

The talent pool is smaller

Yes, there are far fewer experienced Svelte devs out there than there are React devs.

However, I would argue that it takes far less time for the average developer to learn Svelte and become productive with it than it does to learn React.

I would also argue that any developer that has taken the time to teach themselves Svelte probably has great taste and admires clean code (looks in mirror, does a cheesy wink to camera 😉).

The Svelte ecosystem isn’t as “rich” as React’s

Sorry Svelte, but yes, this one is categorically true. You are going to find far more libraries for “doing _ in React” than “doing _ in Svelte”.

This is probably the strongest case against Svelte, which is saying something becomes it amounts to a catch-22 of “people aren’t building libraries for your framework because it isn’t popular enough, and we don’t want to help it become popular because it doesn’t have enough 3rd party libraries”.

Sidenote: I’ve never been a fan of the “because its popular” argument. Battle-tested and robust? Sure, great arguments! “Because the popular kids are using it”? Less so.

Reminder: Facebook built React to solve Facebook-specific and Facebook-sized problems. The same idea applies to a lot of frameworks; always think carefully about the actual problems a tool is solving before picking it to apply to your use-case.

Conclusion / Wrap-Up

Conclusion: You are silly goose if you don’t use Svelte.

JUST KIDDING. My go-to sayings are “use the right tool for the job” and “strong opinions, loosely held”, so ultimately I think you should use whatever framework is the best fit for your specific project. Maybe it’s Svelte, maybe it’s React, maybe it’s something completely different like Vue or Angular.

But I also think people are coming around and I’m not alone in this; Svelte has consistently scored highly in developer surveys for being “desired”.

At the end of the day, I value simple, clean code that is easy to write, read, and maintain – for me personally, Svelte is the framework in which I find this the easiest to achieve, with the least amount of time and effort.

Further Reading

Thanks for Reading 👋

I know this was a lengthy post – one could say… it’s not so svelte (ducks tomatoes being thrown, audible boos).

Anyways, thanks for reading and I hope even if you don’t agree with all my points, you at least got something useful out of it.

Leave a Reply

Your email address will not be published.