The other day, Josh “Triple-Threat” Collinsworth challenged me in the ShopTalkShow D-d-d-discord on why I always caveat Svelte with “if you’re comfortable with the compiler” (which I said in Ep537):

Assuming you’re using a compiler already, why might Svelte’s [compiler] not be a good choice? What obstacles does that create? … I mean, if you’re using single-file components with Vue, you’re already bought into a compiler. So why is it a fear with Svelte and not with Vue? Am I missing something, or are those not just the same thing?

Oof. He’s right. I am almost certainly applying a double standard and over-thinking the issue. At a high-level, there’s nothing different between Vue and Svelte’s compiler; code goes in → command gets run → website comes out. Why do I have reservations about Svelte but am permissive of Vue?

First off, I think Svelte is great. I have pre-existing biases about too much tooling in JavaScript, but I appreciate that Svelte was the first to say “It’s not just a view library, it’s a whole danged compiler.” That compiler is the secret sauce that allows you to write and send small, succinct, efficiently optimized code to the client. It works in spades. Svelte sites are frequently faster and more performant than alternative frameworks.

Why do I give Vue a pass? As Josh pointed out, using Vue Single-File Components (SFCs) have me vendor-locked to Vue’s compiler. I’ve mulled this over a bit and I think for me it comes down to inputs and outputs. Here’s how Vue SFCs work (in my head):

<!-- Input -->
<template>
  <div>hello {{name}}!</div>
</template>

<script>
export default {
  data() {
    return { name: 'world' }
  }
}
</script>
// Output
Vue.component({
  data() {
    return { name: 'world' }
  },
  template: `<div>hello {{name}}!</div>`
})

The code that goes in is similar to the code that comes out. A simple reformatting. I could (and have) written code like that to run in the browser without a build process.

When I look at the Svelte REPL hello world

<!-- Input -->
<script>
	let name = 'world';
</script>

<h1>Hello {name}!</h1>
// Output
/* App.svelte generated by Svelte v3.52.0 */
import {
	SvelteComponent,
	detach,
	element,
	init,
	insert,
	noop,
	safe_not_equal
} from "svelte/internal";

function create_fragment(ctx) {
	let h1;

	return {
		c() {
			h1 = element("h1");
			h1.textContent = `Hello ${name}!`;
		},
		m(target, anchor) {
			insert(target, h1, anchor);
		},
		p: noop,
		i: noop,
		o: noop,
		d(detaching) {
			if (detaching) detach(h1);
		}
	};
}

let name = 'world';

class App extends SvelteComponent {
	constructor(options) {
		super();
		init(this, options, null, create_fragment, safe_not_equal, {});
	}
}

export default App;

With the Svelte compiler, the code I put in and the code I get out are very different. I can reason my way through the code, but it does not look like code I would ever write myself. The output feels more like a low-level assembly language and while it’s probably fine, having a large delta between inputs and outputs worries me about an increased surface area for potential problems down the road: compiler errors, production-only bugs, or breaking builds.

Again, Svelte is great. This code is unbelievably efficient both in inputs and outputs. I like <svelte:options tag="my-counter"/> for making distributable web components and the $: syntactic sugar for reactive properties is handy, but over time I feel these non-Ecmascript shortcuts create a deeper dependency on the Svelte compiler and make your code less portable. I don’t know enough about mature Svelte apps, but I’d be curious if they look like JavaScript code or “Svelte code”. This is not meant to be a judgement or demonizing, I’m hesitant about adding TypeScript to my projects for the same lock-in reasons.

What the Vue compiler is actually doing…

Time for a bit more honesty. Remember when I said “Here’s how Vue SFCs work (in my head)” earlier? Well emphasis on the “in my head” part. How the Vue compiler actually works is a bit different:

<!-- Input -->
<template>
  <div>hello {{name}}!</div>
</template>

<script>
export default {
  data() {
    return { name: 'world' }
  }
}
</script>
// Output
const __sfc__ = {
  data() {
    return { name: 'world' }
  }
}

import { 
	toDisplayString as _toDisplayString, 
	openBlock as _openBlock, 
	createElementBlock as _createElementBlock 
} from "vue"

function render(_ctx, _cache, $props, $setup, $data, $options) {
  return (
		_openBlock(), 
		_createElementBlock(
			"div", 
			null, 
			"hello " + _toDisplayString($data.name) + "!", 
			1 /* TEXT */
		)
	)
}

__sfc__.render = render
__sfc__.__file = "App.vue"

export default __sfc__

Vue precompiles the template and directives into a render() function. That function looks simple here, but in more complex templates is a nested snarl of createElementBlock , createElementVNode and createTextVnode functions.

There’s a weird subjective phenomenon at work. How Vue’s compiler actually works is not that much different than what Svelte is doing. However, how I understand Vue’s compiler to work still falls within my Overton window for what’s acceptable for a codemod. I think developers have drastically different Overton windows.

tl;dr

This is a long-winded way to say I think Vue’s compiler is more like a Source-to-Source compiler (a transpiler, keeping the abstraction at the same level) and Svelte is like a Bytecode compiler (non-human readable, designed for efficient execution). And while the interface is the same (write code → run command → get code), there are subtle differences there.

The more you leverage a compilation process, the more you start writing code for the compiler that has no chance of portability and that I feel has problems down the road. Think of how no one at the company wants to touch the Webpack config for fear of it all falling down. Every shortcut is a potential pain point in a refactor down the road.

There’s a general issue about complexity and introducing another level of abstraction when it’s not necessary. As the fundamental theory of software engineering and its correlary states:

“We can solve any problem by introducing an abstraction layer… except for the problem of too many abstraction layers”

Regardless of how convenient something is to add into my build process, I still want to abide in the Rule of Least Power as a programming principle. My ideal coding environment is to have zero build processes. Those who build with Web Components have tasted this buildless future. The before/after for a Lit web component looks like this…

// Input
import { LitElement, html } from 'lit'

class MyComponent extends LitElement {
  constructor() {
		super();
		this.name = 'world'
  }

	render() {
		return html`<div>hello ${name}!</div>`
	}
}
// Output
import { LitElement, html } from '/node_modules/lit/index.js'

class MyComponent extends LitElement {
  constructor() {
		super();
		this.name = 'world'
  }

	render() {
		return html`<div>hello ${name}!</div>`
	}
}

The inputs and outputs are the same except we’ve converted the non-standard npm module magic package link to now point to a real file on the computer. Import Maps make this even better. The age-old argument that “you’re going to use a compiler anyways” isn’t true anymore. My ideal “compiler” right now might be something that generates an import map and injects it into my pages then touches nothing else except possibly minifying (without uglification).

This isn’t to dissuade people from using the tools they want to use. This is me explaining why I caveat or kick against compilation and build processes in general. I probably need to unpack some emotional baggage from past decades of work doing ObjC, .Net, and Java client work. Those were not my favorite projects and I think I will always prefer lightweight, non-typed scripting languages. I also over-index on code portability and the hypothetical need to migrate away from a technology, that’s on me as well.

Anyways, my criticisms aren’t about Svelte or Vue and probably more general about compilers themselves. I’m tired of them. I’ve built a lot of code modification pipelines over the years and you know what always breaks down? The code modification pipelines.

Alas, I write this in the full and complete irony of despite being so defensive about compiler and vendor lock-in, I’m stuck on Nuxt 2 because the Nuxt 2 → Nuxt 3 migration is too difficult for my app right now. Hoisted by my own framework petard.