Thoughts on Vue vs Svelte

Thursday, the twenty-ninth of June, A.D. 2023

R ecently I’ve had a chance to get to know Vue a bit. Since my frontend framework of choice has previously been Svelte (this blog is built in Svelte, for instance) I was naturally interested in how they compared.

This is necessarily going to focus on a lot of small differences, because Vue and Svelte are really much more similar than they are different. Even among frontend frameworks, they share a lot of the same basic ideas and high-level concepts, which means that we get to dive right into the nitpicky details and have fun debating bind:attr={value} versus :attr="value". In the meantime, a lot of the building blocks are basically the same or at least have equivalents, such as:

  • Single-file components with separate sections for markup, style, and logic
  • Automatically reactive data bindings
  • Two-way data binding (a point of almost religious contention in certain circles)
  • An “HTML-first” mindset, as compared to the “Javascript-first” mindset found in React and its ilk. The best way I can describe this is by saying that in Vue and Svelte, the template wraps the logic, whereas in React, the logic wraps the template.

I should also note that everything I say about Vue applies to the Options API unless otherwise noted, because that’s all I’ve used. I’ve only seen examples of the Composition API (which looks even more like Svelte, to my eyes), I’ve never used it myself.

With that said, there are plenty of differences between the two, and naturally I find myself in possession of immediate and vehement Preferences.

1 Completely arbitrary, of course, so feel free to disagree!
Starting with:

Template Syntax

Overall I think I favor Vue here. Both Vue and Svelte expect you to write most of your code in “single-file components”, which are collections of markup, style, and logic

2 Much like a traditional HTML page.
that work together to describe the appearance and behavior of a component. But naturally, they do it slightly differently. Vue adds custom vue-specific attributes directly to the HTML elements, such as:

<div v-if="items.length">
    <p>Please choose an item.</p>
    <ul>
        <li v-for="item in items">{{ item.name }}</li>
    </ul>
</div>
<div v-else>
    <p>There are no items available.</p>
</div>

While Svelte takes the more common approach of wrapping bits of markup in its own templating constructs:

{#if items.length}
<div>
    <p>Please choose an item</p>
    <ul>
        {#each items as item}
        <li>{item.name}</li>
    </ul>
</div>
{:else}
<div>
    <p>There are no items available.</p>
</div>

While Vue’s approach may be a tad unorthodox, I find that I actually prefer it in practice. It has the killer feature that, by embedding itself inside the existing HTML, it doesn’t mess with my indentation - which is something that has always bugged me about Mustache, Liquid, Jinja, etc.

3 Maybe it’s silly of me to spend time worrying about something so trivial, but hey, this whole post is one big bikeshed anyway.

Additionally (and Vue cites this as the primary advantage of its style, I think) the fact that Vue’s custom attributes are all syntactically valid HTML means that you can actually embed Vue templates directly into your page source. Then, when you mount your app to an element containing Vue code, it will automatically figure out what to do with it.

4 AlpineJS also works this way, but this is the only way that it works - it doesn’t have an equivalent for Vue’s full-fat “app mode” as it were.
This strikes me as a fantastic way to ease the transition between “oh I just need a tiny bit of interactivity on this page, so I’ll just sprinkle in some inline components” and “whoops it got kind of complex, guess I have to factor this out into its own app with a build step and all now.”

Detractors of this approach might point out that it’s harder to spot things like v-if and v-for when they’re hanging out inside of existing HTML tags, but that seems like a problem that’s easily solved with a bit of syntax highlighting. However I do have to admit that it’s a reversal of the typical order in which you read code: normally you see the control-flow constructs first, and only after you’ve processed those do you start to worry about whatever they’re controlling. So you end up with a sort of garden-path-like problem where you have to mentally double back and re-read things in a different light. I still don’t think it’s a huge issue, though, because in every case I’m come across the control flow bits (so v-if, v-for, and v-show) are specified immediately after the opening tag. So you don’t really have to double back by an appreciable amount, and it doesn’t take too long to get used to it.

Continuing the exploration of template syntax, Vue has some cute shorthands for its most commonly-used directives, including : for v-bind and @ for v-on. Svelte doesn’t really have an equivalent for this, although it does allow you to shorten attr={attr} to {attr}, which can be convenient. Which might as well bring us to:

Data Binding

I give this one to Svelte overall, although Vue has a few nice conveniences going for it.

Something that threw me a tiny bit when I first dug into Vue was that you need to use v-bind on any attribute that you want to have a dynamic value. So for instance, if you have a data property called isDisabled on your button component, you would do <button v-bind:disabled="isDisabled"> (or the shorter <button :disabled="isDisabled">).

The reason this threw me is that Svelte makes the very intuitive decision that since we already have syntax for interpolating variables into the text contents of our markup, we can just reuse the same syntax for attributes. So the above would become <button disabled={isDisabled}>, which I find a lot more straightforward.

5 If your interpolation consists of a single expression you can even leave off the quote marks (as I did here), which is pleasant since you already have {} to act as visual delimiters.
I also find it simpler in cases where you want to compose a dynamic value out of some fixed and some variable parts, e.g. <button title="Save {itemsCount} items"> vs. <button :title="`Save ${itemsCount} items`">.

Two-way bindings in Svelte are similarly straightforward, for example: <input type="checkbox" bind:checked={isChecked}> In Vue this would be <input type="checkbox" v-model="isChecked">, which when you first see it doesn’t exactly scream that the value of isChecked is going to apply to the checked property of the checkbox. On the other hand, this does give Vue the flexibility of doing special things for e.g. the values of <select> elements: <select v-model="selectedOption"> is doing quite a bit of work, since it has to interact with not only the <select> but the child <option>s as well. Svelte just throws in the towel here and tells you to do <select bind:value={selectedOption}>, which looks great until you realize that value isn’t technically a valid attribute for a <select>. So Svelte’s vaunted principle of “using the platform” does get a little bent out of shape here.

Oh, and two-way bindings in Vue get really hairy if it’s another Vue component whose attribute you want to bind, rather than a built-in form input. Vue enforces that props be immutable from the inside, i.e. a component isn’t supposed to mutate its own props. So from the parent component it doesn’t look too bad:

<ChildComponent v-model="childValue" />`

But inside the child component:

export default {
    props: ['modelValue'],
    emits: ['update:modelValue'],
    methods: {
        doThing() {
            this.$emit('update:modelValue', newValue)
        }
    }
}

In Svelte, you just bind: on a prop of a child component, and then if the child updates the prop it will be reflected in the parent as well. I don’t think there’s any denying that’s a lot simpler.

6 I think this is where the “two-way data binding” holy wars start to get involved, but I actually really like the way Svelte does things here. I think most of the furor about two-way data binding refers to bindings that are implicitly two-way, i.e. anyone with a reference to some value can mutate it in ways the original owner didn’t expect or intend it to. (KnockoutJS observables work this way, I think?) In Svelte’s case, though, this is only possible if you explicitly pass the state with bind:, which signifies that you do want this state to be mutated by the child and that you have made provisions therefor. My understanding is that in React you’d just be emitting an event from the child component and handling that event up the tree somewhere, so in practice it’s basically identical. That said, I haven’t used React so perhaps I’m not giving the React Way™ a fair shake here.

Vue does have some lovely convenience features for common cases, though. One of my favorites is binding an object to the class of an HTML element, for example: <button :class="{btn: true, primary: false}"> Which doesn’t look too useful on its own, but move that object into a data property and you can now toggle classes on the element extremely easily by just setting properties on the object. The closest Svelte comes is <button class:btn={isBtn} class:primary={isPrimary}>, which is a lot more verbose. Vue also lets you bind an array to class and the elements of the array will be treated as individual class names, which can be convenient in some cases if you have a big list of classes and you’re toggling them all as a set.

7 Since I’m a fan of TailwindCSS, this tends to come up for me with some regularity.

The other area where I vastly prefer Vue’s approach over Svelte’s is in event handlers. Svelte requires that every event handler be a function, either named or inline, so with simple handlers you end up with a lot of <button on:click={() => counter += 1} situations. Vue takes the much more reasonable approach of letting you specify a plain statement as your event handler, e.g. <button @click="counter += 1">. For whatever reason this has always particularly annoyed me about Svelte, so Vue’s take is very refreshing.

Admittedly, the Svelte approach does lead more gracefully into more complex scenarios where you need to capture the actual JS event: it just gets passed to the function. Vue kind of capitulates on consistency here and also lets you pass the name of a function to an event handler, which is then called with the event as an argument. Oooor, you can reference the event via the special variable $event, which is convenient but feels a bit shoehorned in.

I’m ragging on Vue for its inconsistency here but I should note that I still do prefer the Vue approach, warts and all. “A foolish consistency is the hobgoblin of small minds,” after all, and Vue’s syntax is just so convenient. Besides, it optimizes for the 95% of the time I don’t care about capturing the event, because realistically when am I going to want to do that? In both Vue and Svelte, all the traditional use cases for capturing an event are solved in other ways:

  • You don’t usually need event.target, because you can just give yourself a handle to the element directly (via ref in Vue, bind:this= in Svelte)
  • You don’t need to use it to get the value of an input (common with events like change), because you’re just going to use a two-way binding for that
  • In Vue, you don’t even need it to check for modifier keys, because Vue gives you special syntax for this like @event.shift. (Svelte doesn’t have an equivalent for this, so advantage Vue here again.)

You really only need to access the event when you’re doing something more exotic, e.g. handling a bubbling event on a parent element and you need to check which child was actually the target, which does happen but again not the majority of the time.

Declaring Reactive Values

In Vue, reactive values (by which I mean “values that can automatically trigger a DOM update when they change”) are either passed in as props, or declared in data. Or derived from either of those sources in computed. Then you reference them, either directly in your template or as properties of this in your logic. Which works fine, more or less, although you can run into problems if you’re doing something fancy with nested objects or functions that get their own this scope.

8 It’s worth noting that the Composition API avoids this, at the cost of having to call ref() on everything and reference reactiveVar.value rather than reactiveVar by itself.
The split between how you access something from the template and how you access it from logic was a touch surprising to me at first, though.

In Svelte, variables are just variables, you reference them the same way from everywhere, and if they need to be reactive it (mostly) just happens automagically.

9 And of course, after I first wrote this but just before I was finally ready to publish, Svelte went ahead and changed this on me. I’ll leave my comments here as I originally wrote them, just keep in mind that if these changes stick then Svelte becomes even more similar to Vue’s composition API.
Svelte has a lot more freedom here because it’s a compiler, rather than a library, so it can easily insert calls to its special $$invalidate() function after any update to a value that needs to be reactive.

Both frameworks allow you to either derive reactive values from other values, or just execute arbitrary code in response to data updates. In Vue these are two different concepts - derived reactive values are declared in computed, and reactive statements via the watch option. In Svelte they’re just the same thing: Prefix any statement with $: (which is actually valid JS, as it turns out) and it will automatically be re-run any time one of the reactive values that it references gets updated. So both of the following:

$: let fullname = `${firstname} ${lastname}`;
$: console.log(firstname, lastname);

would re-run any time firstname or lastname is updated, assuming those are reactive values to begin with.

Overall I tend to prefer the simplicity of Svelte’s approach to reactivity, although I do find the $: syntax a little weird. It may be valid JS, but it’s not valid JS that anybody actually uses. Moreover its official meaning doesn’t have anything to do with what Svelte is using it for, so the fact that iT’s vAliD jAVaSCriPt doesn’t really do much for me. I think Vue’s computed and watch options are much more obvious, if only from how they’re named.

That said, I don’t have any better ideas for marking reactive statements in Svelte, especially given that sometimes you want a statement to ignore updates even if it does reference a value that might be updated. So maybe this is just one of those compromises you have to make.

Code Structure

I go back and forth on this one, but I think I have a slight preference for Svelte (at least, at the moment.) The major difference is that Vue

10 If you’re using the Options API, at least.
enforces a lot more structure than Svelte: Data is in props/data/computed, logic is in methods, reactive stuff is in watch, etc. Svelte, by contrast, just lets you do basically whatever you want. It does require that you have only one <script> tag, so all your logic ends up being co-located, but that’s pretty much it. Everything else is just a convention, like declaring props at the top of your script.

The advantage of Vue’s approach is that it can make it easier to find things when you’re jumping from template to logic: you see someFunction(whatever), you know it’s going to be under methods. With Svelte, someFunction could be defined anywhere in the script section.

11 Code structure is actually one area that I think might be improved by the recently-announced Svelte 5 changes: Because you can now declare reactive state anywhere, rather than just at the top level of your script, you can take all the discrete bits of functionality within a single component and bundle each one up in its own function, or even factor them out into different files entirely. I can imagine this being helpful, but I haven’t played with it yet so I don’t know for sure how it will shake out.

On the other hand, this actually becomes a downside once your component gets a little bit complex. Separation of concerns is nice and all, but sometimes it just doesn’t work very well to split a given component, and it ends up doing several unrelated or at least clearly distinct things. In Vue-land, the relevant bits of state, logic, etc. are all going to be scattered across data/methods/etc, meaning you can’t really see “all the stuff that pertains to this one bit of functionality” in one place. It’s also very clunky to split the logic for a single component across multiple JS files, which you might want to do as another way of managing the complexity of a large component. If you were to try, you’d end up with a big “skeleton” in your main component file, e.g.

export default {
    import {doThing, mungeData} from './otherfile.js';

    // ...
    computed: {
        mungeData,
        // ...
    }
    methods: {
        doThing,
        // ...
    },
}

which doesn’t seem very pleasant.

As a matter of fact, this was one of the primary motivations

12 Archive link, since that url now redirects to the current Composition API FAQ.
for the introduction of the Composition API in the first place. Unfortunately it also includes the downside that you have to call ref() on all your reactive values, and reference them by their .value property rather than just using the main variable. It’s funny that this bothers me as much as it does, given that this.someData is hardly any more concise than someData.value, but there’s no accounting for taste, I guess. Using this just feels more natural to me, although what feels most natural is Svelte’s approach where you don’t have to adjust how you reference reactive values at all.

Also, as long as we’re harping on minor annoyances: For some reason I cannot for the life of me remember to put commas after all my function definitions in computed, methods etc. in my Vue components. It’s such a tiny thing, but it’s repeatedly bitten me because my workflow involves Vue automatically rebuilding my app every time I save the file, and I’m not always watching the console output because my screen real estate is in use elsewhere.

13 E.g. text editor on one screen with two columns of text, web page on one half of the other screen and dev tools on the other half. Maybe I need a third monitor?
So I end up forgetting a comma, the rebuild fails but I don’t notice, and then I spend five minutes trying to figure out why my change isn’t taking effect before I think to check for syntax errors.

It would be remiss of me, however, not to point out that one thing the Vue Options API enables

14 Kind of its initial raison d’être, from what I understand.
which is more or less impossible
15 I mean, you could do it, but you’d have to ship the entire Svelte compiler with your page.
with Svelte is at-runtime or “inline” components, where you just stick a blob of JS onto your page that defines a Vue component and where it should go, and Vue does the rest on page load. Svelte can’t do this because it’s a compiler, so naturally it has to compile your components into a usable form. This has many advantages, but sometimes you don’t want to or even can’t add a build step, and in those cases Vue can really shine.

Miscellany

Performance

Performance isn’t really a major concern for me when it comes to JS frameworks, since I don’t tend to build the kind of extremely-complex apps where the overhead of the framework starts to make a difference. For what it’s worth, though, the Big Benchmark List has Vue slightly ahead of Svelte when it comes to speed.

16 Although recent rumors put the next major version of Svelte very close to that of un-framework’d vanilla JS, so this might change in the future.
I don’t know how representative this benchmark is of a real-world workload.

As far as bundle size goes, it’s highly dependent on how many components you’re shipping - since Svelte compiles everything down to standalone JS and there’s no shared framework, the minimum functional bundle can be quite small indeed. The flipside is that it grows faster with each component than Vue, again because there’s no shared framework to rely on. So a Svelte app with 10 components will probably be a lot smaller than the equivalent Vue app, but scale that up to 1000 components and the advantage will most likely have flipped. The Svelte people say that this problem doesn’t tend to crop up a lot in practice, but I have yet to see real-world examples for the bundle size of a non-trivial

17 Probably because no one wants to bother implementing the exact same app in two different frameworks just to test a theory.
app implemented in Vue vs. Svelte.

Ecosystem

Vue has been around longer than Svelte, so it definitely has the advantage here. That said, Svelte has been growing pretty rapidly in recent years and there is a pretty decent ecosystem these days. This blog, for instance, uses SvelteKit and mdsvex. But there are definitely gaps, e.g. I wasn’t able to find an RSS feed generator when I went looking.

18 Arguably this is a lack in the SvelteKit ecosystem rather than the Svelte ecosystem, but I think it’s fair to lump it together. SvelteKit is dependent on Svelte, so naturally it inherits all of Svelte’s immaturity issues plus more of its own.
If I’d been using Vue/Nuxt it would have been available as a first-party integration. All in all I’d say if a robust ecosystem is important to you then Vue is probably the better choice at this point.

Stability

Not in terms of “will it crash while you’re using it,” but in terms of “will code that you write today still be usable in five years.” This is always a bit of a big ask in the JS world, because everyone is always pivoting to chase the new shiny. As I write this now (and as I referenced above), Svelte has just announced a change to how reactivity is done. The new style is opt-in for the moment, but that’s never completely reassuring—there are plenty of examples of opt-in features that became required eventually. Vue had a similar moment with their 2-to-3 switch,

19 Just like Python, hmm. What is it about the 2-to-3 transition? Maybe we should call it Third System Effect?
but to be fair they have so far stuck to their promise to keep the Options API a first-class citizen.

I think that means I have to give Vue the edge on this one, because while both frameworks now have an “old style” vs. a “new style” Vue at least has proven their willingness to continue supporting the old style over the last few years.

What’s Next

I don’t think we’ve reached the “end-game” when it comes to UI paradigms, either on the web or more generally. I do think that eventually, probably within my lifetime, we will see a stable and long-lasting consensus emerge, and the frenetic pace of “framework churn” in the frontend world will slow down somewhat. What exact form this will take is very much up in the air, of course, but I have a sneaking suspicion that WebAssembly will play a key part, if it can ever get support for directly communicating with the DOM (i.e. without needing to pass through the JS layer). If and when that happens, it will unlock a huge new wave of frontend frameworks that don’t have to involve on Javascript at all, and won’t that be interesting?

But for now I’ll stick with Svelte, although I think Vue is pretty good too. Just don’t make me use React, please.