finish vue vs svelte post

This commit is contained in:
Joseph Montanaro 2023-09-24 09:20:15 -07:00
parent 5ec147f95f
commit 46c0792cf1
2 changed files with 25 additions and 12 deletions

View File

@ -2,6 +2,7 @@
title: Sidenotes
description: An entirely-too-detailed dive into how I implemented sidenotes for this blog.
date: 2023-08-14
draft: true
---
<script>
import Sidenote from '$lib/Sidenote.svelte';

View File

@ -14,11 +14,11 @@ Of course, this is only possible because Vue and Svelte are really much more sim
* 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<Sidenote>Or single-file component, anyway.</Sidenote> embeds the logic, whereas in React, the logic embeds the template.
* 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.<Sidenote>I should also clarify that practically everything in this post is just that: a preference. While I obviously plan to explain my preferences and think it would be reasonable for other people to do the same, it's undeniably true that preferences can vary, and in a lot of cases are basically arbitrary. So if you find yourself disagreeing with all or most of what I say, consider it an opportunity to peer into the mindset of The Other Side.</Sidenote> Starting with:
With that said, there are plenty of differences between the two, and naturally I find myself in possession of immediate and vehement Preferences.<Sidenote>Completely arbitrary, of course, so feel free to disagree!</Sidenote> Starting with:
## Template Syntax
@ -52,11 +52,11 @@ While Svelte takes the more common approach of wrapping bits of markup in its ow
</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.<Sidenote>Maybe it's silly of me to spend time worrying<Sidenote>Nested<Sidenote>Doubly-nested sidenote!</Sidenote> sidenote!</Sidenote> about something so trivial,<Sidenote>Second nested sidenote.</Sidenote> but hey, this whole post is one big bikeshed anyway.</Sidenote>
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.<Sidenote>Maybe it's silly of me to spend time worrying about something so trivial, but hey, this whole post is one big bikeshed anyway.</Sidenote>
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.<Sidenote>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.</Sidenote> 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.<Sidenote>I'm being unfair here. It's more than just a lack of syntax highlighting, it's a reversal of the typical order in which people are used to reading code, where the control flow is indicated before whatever it's controlling. So you end up with a sort of [garden-path-like](https://xkcd.com/2793/) 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.</Sidenote>
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](https://xkcd.com/2793/) 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:
@ -70,7 +70,7 @@ The reason this threw me is that Svelte makes the very intuitive decision that s
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 builtin 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:
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:
```markup
<ChildComponent v-model="childValue" />`
```
@ -87,9 +87,9 @@ export default {
}
```
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.<Sidenote>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. the child can mutate state that the parent didn't expect or intend it to. 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. </Sidenote>
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.<Sidenote>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 tosome stat 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.</Sidenote>
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.
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. <Sidenote>Since I'm a fan of TailwindCSS, this tends to come up for me with some regularity.</Sidenote>
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.
@ -106,7 +106,7 @@ You really only need to access the event when you're doing something more exotic
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.<Sidenote>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.</Sidenote> 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. 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.
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.<Sidenote>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](https://svelte.dev/blog/runes). 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.</Sidenote> 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:
```js
@ -123,7 +123,7 @@ $: console.log(firstname, lastname);
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<Sidenote>If you're using the Options API, at least.</Sidenote> 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.
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.<Sidenote>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.</Sidenote>
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.
@ -145,11 +145,11 @@ export default {
which doesn't seem very pleasant.
As a matter of fact, this was one of the primary [motivations](https://web.archive.org/web/20201109010309/https://composition-api.vuejs.org/#logic-reuse-code-organization) for the introduction of the Composition API in the first place.<Sidenote>Archive link, since that url now redirects to the [current Composition API FAQ](https://vuejs.org/guide/extras/composition-api-faq.html).</Sidenote> 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.
As a matter of fact, this was one of the primary [motivations](https://web.archive.org/web/20201109010309/https://composition-api.vuejs.org/#logic-reuse-code-organization)<Sidenote>Archive link, since that url now redirects to the [current Composition API FAQ](https://vuejs.org/guide/extras/composition-api-faq.html).</Sidenote> 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.<Sidenote>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?</Sidenote> 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<Sidenote>Kind of its initial _raison d'être_, from what I understand.</Sidenote> which is completely impossible 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.
It would be remiss of me, however, not to point out that one thing the Vue Options API enables<Sidenote>Kind of its initial _raison d'être_, from what I understand.</Sidenote> which is more or less impossible<Sidenote>I mean, you could do it, but you'd have to ship the entire Svelte compiler with your page.</Sidenote> 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
@ -157,8 +157,20 @@ It would be remiss of me, however, not to point out that one thing the Vue Optio
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](https://krausest.github.io/js-framework-benchmark/current.html) has Vue slightly ahead of Svelte when it comes to speed.<Sidenote>Although [recent rumors](https://twitter.com/Rich_Harris/status/1688581184018583558) put the next major version of Svelte _very_ close to that of un-framework'd vanilla JS, so this might change in the future.</Sidenote> 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.
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<Sidenote>Probably because no one wants to bother implementing the exact same app in two different frameworks just to test a theory.</Sidenote> 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](https://kit.svelte.dev) and [mdsvex](https://mdsvex.pngwn.io/). But there are definitely gaps, e.g. I wasn't able to find an RSS feed generator when I went looking.<Sidenote>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.</Sidenote> If I'd been using Vue/Nuxt it would have been available as a [first-party integration](https://content.nuxtjs.org/v1/community/integrations). 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](https://svelte.dev/blog/runes) 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,<Sidenote>Just like Python, hmm. What is it about the 2-to-3 transition? Maybe we should call it Third System Effect?</Sidenote> 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.