Vue vs Svelte: Comparing Framework Internals

What is Svelte

Svelte is the latest JavaScript framework that has been making waves in the web development world. Chances are you have probably heard of it and its reputation of being “very fast.”

In a nutshell, a Svelte app is indeed faster than the apps created using all other frontend frameworks (including jQuery, if you consider that a framework). By “faster,” I’m referring to the time that it takes to download the bundle in production, the time to load the bundle in the browser, and the time to update the UI every time some reactive data change. (We’ll focus on the last one in this tutorial because it’s more prevalent)

The driving force behind its impressive performance is the framework’s implementation approach.

https://firebasestorage.googleapis.com/v0/b/vue-mastery.appspot.com/o/flamelink%2Fmedia%2F1.1621450427274.jpg?alt=media&token=3274525f-a102-4a82-894e-a168b2c2226e

In short, Svelte is not a framework, it’s a compiler. And technically speaking, Svelte code is not real JavaScript code. So basically, the compiler takes the Svelte code as input and generates the actual JavaScript code for production. We’ll expand on this in a bit, but often it’s a lot simpler to learn something new when comparing it against something familiar. So in this tutorial, we’ll put the two frameworks, Vue and Svelte, side-by-side, and compare the different implementation approaches.

https://firebasestorage.googleapis.com/v0/b/vue-mastery.appspot.com/o/flamelink%2Fmedia%2F2.1621450427275.jpg?alt=media&token=fd54c9e9-35b6-48a9-a557-d74ab171ac84


Sample Apps

Before we get into the finer points of either framework, let’s start with some code.

The Vue App

You can get the sample Vue project from GitHub by running this command:

git clone --branch full-name-vue https://github.com/Code-Pop/blog.git my-vue-app

This will generate a project called my-vue-app in your computer.

To run the Vue app:

cd my-vue-app
npm install
npm run serve

This Vue app will now be live at localhost:8080


The Svelte App

We’ll run another git clone command to get the sample Svelte project, but from a different branch and with a different folder name:

git clone --branch full-name-svelte https://github.com/Code-Pop/blog.git my-svelte-app

For future reference, you can use the following command to create a brand new Svelte app:

npx degit sveltejs/template my-svelte-app

This basically downloads a project template from Svelte’s GitHub account.

To run our sample Svelte app:

cd my-svelte-app
npm install
npm run dev

This Svelte app is now live at localhost:5000

https://firebasestorage.googleapis.com/v0/b/vue-mastery.appspot.com/o/flamelink%2Fmedia%2F3.1621450432378.jpg?alt=media&token=845874f6-9152-4bbf-a61b-ede44e0075bc

These two simple sample apps look the same and have the exact same features:

  • If you enter your first and last name, the app will display your full name.
  • You can click the Reset button to empty out the input fields.

To start comparing them, let’s look at the source files of both apps.


The Code

For the Vue app, the main code is located inside the App.vue file: 📃 /my-vue-app/src/App.vue

<template>
  <main>
    <p>Hi, please enter your name:</p>
    <p>First: <input v-model="firstName" /></p>
    <p>Last: <input v-model="lastName" /></p>
    <p>{{ fullName }}</p>
    <p><button v-on:click="reset">Reset</button></p>
  </main>
</template>

<script>
export default {
  data() {
    return {
      firstName: '',
      lastName: ''
    }
  },
  computed: {
    fullName() {
      return this.firstName + ' ' + this.lastName
    }
  },
  methods: {
    reset() {
      this.firstName = ''
      this.lastName = ''
    }
  }
}
</script>

For the Svelte app, the main code is located in the App.svelte file: 📃 /my-svelte-app/src/App.svelte

<script>
  let firstName = '';
  let lastName = '';

  $: fullName = firstName + ' ' + lastName;

  function reset() {
    firstName = '';
    lastName = '';
  }
</script>

<main>
  <p>Hi, please enter your name:</p>
  <p>First: <input bind:value={firstName} /></p>
  <p>Last: <input bind:value={lastName} /></p>
  <p>{ fullName }</p>
  <p><button on:click={reset}>Reset</button></p>
</main>

While the Vue code has more lines, it is descriptive and clear.

The Svelte code is more concise, but it can be a bit cryptic if this is your first time using it.

Both versions have the same basic ingredients:

  • Two reactive variables firstName and lastName
  • One computed variable fullName based on the two reactive variables
  • Two input fields bound to the reactive variables
  • A button that triggers an event handler to reset the reactive variables

Next, we’ll go through the various syntactical elements in the Svelte code and decipher them.


Svelte 101

The most obvious thing in the Svelte code is that reactive variables are created without any special API functions.

They look just like regular JavaScript variables:

📃 /my-svelte-app/src/App.svelte

let firstName = '';
let lastName = '';

But whenever you reassign them (such as in the reset function), the component will get re-rendered:

📃 /my-svelte-app/src/App.svelte

function reset() {
  firstName = '';
  lastName = '';
}

So although firstName and lastName look like primitive JavaScript variables, they behave more like observables. Once again, Svelte code is not actual JavaScript code. We’ll discuss what exactly is going on behind the scenes in a moment.

Another signature element in the Svelte syntax is the dollar sign in this line:

📃 /my-svelte-app/src/App.svelte

$: fullName = firstName + ' ' + lastName;

The dollar sign is used to create a computed value called fullName, so whenever firstName or lastName change, fullName will react to their changes. Without using the dollar sign, fullName will be not updated after the initialization.

Next, let’s put Svelte’s template and Vue’s template side by side:

https://firebasestorage.googleapis.com/v0/b/vue-mastery.appspot.com/o/flamelink%2Fmedia%2F4_1.jpg?alt=media&token=3cee6092-e181-46f1-96c3-bcff61d85d00

They are very similar in style, but the names of the directives are different.

Event handling is similar in both frameworks, except that in Vue we have to use the this keyword to access the state. (If we used Vue’s Composition API, we wouldn’t have to use the this keyword)


You’ve probably noticed that we are not using any props in the code. Let’s add a prop to the Svelte component so that we can show/hide the reset feature.

The syntax for props is basically just export and let:

📃 /my-svelte-app/src/App.svelte

export let canReset; // NEW

let firstName = '';
let lastName = '';
...

Although the export keyword is used, it’s not meant to export any data. It’s actually doing the opposite, and importing data as props.

We can assign a default vale to the prop:

📃 /my-svelte-app/src/App.svelte

export let canReset = false; // CHANGE

let firstName = '';
let lastName = '';
...

We’ll use this prop to conditionally render the Reset button:

📃 /my-svelte-app/src/App.svelte

<main>
  ...
  {#if canReset}
    <p><button on:click={reset}>Reset</button></p>
  {/if}
</main>

If you refresh the app in the browser, you would see that the Reset button is now gone because canReset is defaulted to false:

https://firebasestorage.googleapis.com/v0/b/vue-mastery.appspot.com/o/flamelink%2Fmedia%2F5.1621450436878.jpg?alt=media&token=b10cff62-fe87-47d0-b2af-87ad512b58dc

To get it to show the Reset button again, we have to indicate our intention by setting the canReset prop value. And we do that in the main.js file, which is where the App component is getting bootstrapped to the DOM.

Just add a canReset entry to the props property:

📃 /my-svelte-app/src/main.js

import App from './App.svelte';

const app = new App({
  target: document.body,
  props: {
    canReset: true, // NEW
    name: 'world'
  }
});

export default app;

(You can remove the name: 'world' if you want, it’s just part of the Svelte template for demonstration purposes.)

If we go back to the browser now, the Reset button should be back:

https://firebasestorage.googleapis.com/v0/b/vue-mastery.appspot.com/o/flamelink%2Fmedia%2F6.1621450441156.jpg?alt=media&token=c191321e-b665-4d7f-abde-e8d4d65b02c3

As you can see, Svelte’s syntax looks almost like JavaScript, but with so much “magical” behaviors taken care of by the compiler.

Next, we’ll go over the runtime JavaScript code that Svelte compiles to.


Compiled Reactivity

As we’ve seen, Svelte variables don’t have to be declared with a special API function to gain reactivity. By “reactivity,” we usually mean that whenever a variable gets updated, other parts of the code that rely on this variable will get notified.

In our Svelte example, firstName, lastName, and fullName are all reactive variables:

📃 /my-svelte-app/src/App.svelte

let firstName = '';
let lastName = '';

$: fullName = firstName + ' ' + lastName;
...

So the reactive relationship in this little sample application goes like this: whenever firstName or lastName is reassigned, fullName will be updated automatically. Whenever any one of the three variables gets updated, the HTML will get updated automatically. (And of course when the prop canReset is changed from outside the component, the HTML gets updated, too.)

As I’ve mentioned a few times already, that is not real JavaScript because JavaScript variables created in this manner would not be “reactive.” In other frameworks such as Vue and React, we have to use special ways to declare reactive variables. In Vue, we have to use either the data() option or the ref() function from Vue 3’s composition API. In React, we have to use useState.

But since Svelte code gets compiled into JavaScript for production, we can just create reactive variables using plain-old assignment statements, and the compiler will “inject” the needed code into our final JavaScript to make sure that these values are reactive.


Without further ado, Let’s check out the compiled code that the Svelte compiler generates for us, just to get some understanding of how these reactive behaviors are put together.

To see the actual runtime code, we have to pop open Chrome’s DevTools.

(Windows: Ctrl + Shift + J, Mac: Ctrl + Option + J)

And go to the Sources tab.

Then you should see a build folder in the sidebar. Inside that folder, there’s a bundle.js file, and that’s where all the runtime JavaScript resides.

https://firebasestorage.googleapis.com/v0/b/vue-mastery.appspot.com/o/flamelink%2Fmedia%2F7_1.jpg?alt=media&token=106a3f11-05dc-4bb9-ad6e-e37753b5d59b

There are a few hundred lines of code in there, but the part that we’ll focus on is the instance function:

📃 /bundle/bundle.js

function instance($$self, $$props, $$invalidate) {
  let fullName;
  let { $$slots: slots = {}, $$scope } = $$props;
  validate_slots("App", slots, []);
  let { canReset = true } = $$props;
  let firstName = "";
  let lastName = "";

  function reset() {
    $$invalidate(1, firstName = "");
    $$invalidate(2, lastName = "");
  }

  const writable_props = ["canReset"];

  Object.keys($$props).forEach(key => {
    if (!~writable_props.indexOf(key) && key.slice(0, 2) !== "$$") console.warn(`<App> was created with unknown prop '${key}'`);
  });

  function input0_input_handler() {
    firstName = this.value;
    $$invalidate(1, firstName);
  }

  function input1_input_handler() {
    lastName = this.value;
    $$invalidate(2, lastName);
  }

  $$self.$$set = $$props => {
    if ("canReset" in $$props) $$invalidate(0, canReset = $$props.canReset);
  };

  $$self.$capture_state = () => ({
    canReset,
    firstName,
    lastName,
    reset,
    fullName
  });

  $$self.$inject_state = $$props => {
    if ("canReset" in $$props) $$invalidate(0, canReset = $$props.canReset);
    if ("firstName" in $$props) $$invalidate(1, firstName = $$props.firstName);
    if ("lastName" in $$props) $$invalidate(2, lastName = $$props.lastName);
    if ("fullName" in $$props) $$invalidate(3, fullName = $$props.fullName);
  };

  if ($$props && "$$inject" in $$props) {
    $$self.$inject_state($$props.$$inject);
  }

  $$self.$$.update = () => {
    if ($$self.$$.dirty & /*firstName, lastName*/ 6) {
      $$invalidate(3, fullName = firstName + " " + lastName);
    }
  };

  return [
    canReset,
    firstName,
    lastName,
    fullName,
    reset,
    input0_input_handler,
    input1_input_handler
  ];
}

This is the code that gets generated based on our Svelte source code, so you should see variables that have familiar names scattered about, such as firstName, lastName, and fullName. But all in all, it probably looks like gibberish if this is your first time inspecting this compiled bundle.

What we’re most interested in is how the reset function looks like in the runtime JavaScript code, because that’s where the reactive variables are getting reassigned:

https://firebasestorage.googleapis.com/v0/b/vue-mastery.appspot.com/o/flamelink%2Fmedia%2F8.1621450444371.jpg?alt=media&token=4eed2d52-c950-408a-9fae-697beef88cd7

It looks like all the “reactive wiring” is facilitated by the $$invalidate function, which is an internal Svelte function.

What this means is that, right after each assignment, the $$invalidate function is called to inform some other code that a variable has been updated and it should get marked “dirty” (there’s actually a function called make_dirty in the bundle).

Each $$invalidate call sends along a variable ID, 1 is for firstName, 2 is for lastName. You can see all the variables and their corresponding IDs in the $$self.$inject_state function.

📃 /bundle/bundle.js

$$self.$inject_state = $$props => {
  if ("canReset" in $$props) $$invalidate(0, canReset = $$props.canReset);
  if ("firstName" in $$props) $$invalidate(1, firstName = $$props.firstName);
  if ("lastName" in $$props) $$invalidate(2, lastName = $$props.lastName);
  if ("fullName" in $$props) $$invalidate(3, fullName = $$props.fullName);
};

After a variable is marked “dirty,” the update function gets called:

📃 /bundle/bundle.js

$$self.$$.update = () => {
  if ($$self.$$.dirty & /*firstName, lastName*/ 6) {
    $$invalidate(3, fullName = firstName + " " + lastName);
  }
};

So right after Svelte gets informed of a variable change, it will call this update function to decide which variables have to be “refreshed” accordingly. And this is the basic mechanics of Svelte’s internals.


Bitwise operations

As a side note, the update function is actually using a bitwise operation to figure out whether or not fullName needs to be updated. Bitwise operations are basically conditional commands on the bit level, with only 0s and 1s. Bit-level operations make it faster to carry out a series of conditional checks, each with multiple reactive variables.

For example, the number 6 in the above snippet is meant to be used in its binary form, which is 110.

So what does 110 mean?

110 represents a unique combination of the “dirtiness” of all our reactive variables (ordered from right to left by their IDs):

https://firebasestorage.googleapis.com/v0/b/vue-mastery.appspot.com/o/flamelink%2Fmedia%2Fbitwise_dirtiness.jpg?alt=media&token=72e33313-f60d-4c78-94ae-0656ffd487a7

Svelte will use this “dirtiness” configuration, along with a bitwise operator (&), to figure out whether or not the fullName variable has to be updated. We are not going to get into the intricacies of bitwise operations now (you can learn more about it here). But in a nutshell, 6 (or 110) will make the bitwise condition true only if one or more of the red variables (in the picture above) have been marked “dirty.”

In summary, $$invalidate gets called whenever a reactive variable is changed. Then the update function will be called to make sure that any variable that depends on the reactive variable in question gets updated, too.

As you can see, being compiled is the key to Svelte’s performance.

Next, let’s take a look at how Vue is implemented.


Runtime Reactivity

Like most other frontend frameworks, Vue is mostly a runtime framework. That means Vue’s reactivity code is not specific to any variables you create in your own apps. So, the reactivity is implemented in the same library code that all Vue apps have to share.

To implement a reactivity engine for runtime (as opposed to compile-time like Svelte), Vue needed a way to notify the framework when a reactive variable is changed, and it needed a way to efficiently render the changes.

Vue’s implementation is relying on several native JavaScript features and the virtual DOM diffing algorithm.

In a nutshell, Vue’s reactive engine goes like this:

https://firebasestorage.googleapis.com/v0/b/vue-mastery.appspot.com/o/flamelink%2Fmedia%2F10.1621450451870.jpg?alt=media&token=f21df3e7-bc27-4402-b940-99f1b6864cdb

  1. Vue will set up a dedicated object for each entry we provided through the data option. (this is implemented using ES5’s get/set for Vue 2, and ES6’s proxy/reflect for Vue 3).
  2. These dedicated objects have the ability to invoke “handlers” whenever their respective data items are accessed or changed.
  3. When a data item is accessed, the handler will record where it is being accessed. For example, when we call this.firstName while setting up the computed property fullName, we are accessing firstName inside the fullName computed property. So fullName will be recorded in this case. Internally, this linkage is called a “dep”; it means that fullName is depending on firstName.
  4. And whenever a data item such as firstName is changed (as in the reset function), it will trigger a handler in firstName’s dedicated object. The handler will make sure all the values that are depending on firstName also get updated to reflect firstName’s change.
  5. Finally, Vue will render a virtual DOM based on all the new data. A virtual DOM is a lightweight representation of the actual DOM. And then Vue will compare the new virtual DOM tree with the previous virtual DOM tree to figure out which part of the actual DOM needs to be changed. This use of virtual DOM is to prevent unnecessary manipulations of the actual DOM.

This is what it takes for Vue to create reactive values. It’s pretty empowering once you understand how it works. If you want to dig deeper into Vue’s reactivity, you can check out Vue Mastery’s Vue 3 Reactivity course and the Vue 3 Deep Dive with Evan You.

As you probably already noticed, Svelte didn’t do any of this during runtime because of its compiler-oriented approach. And that’s why Svelte is able to perform UI changes so fast.

There are other frontend frameworks that are also compiled, such as Elm. Just like Svelte, Elm is a DSL. But because Elm is still relying on virtual DOM during runtime, it’s not on the same performance level as Svelte.

So it’s not about being a compiler, it’s more about what it compiles to. Svelte gets compiled into code that directly manipulates the DOM without running through the virtual DOM diffing algorithm every time a value changes. Svelte is only able to achieve this as a compiler.

You can think of the Svelte compiler as a very efficient and reliable programmer that scans through the Svelte code and writes you a vanilla JavaScript version of the app. So a Svelte app in runtime is not that different from a jQuery app that you made ten years ago, but the source code in Svelte is more maintainable.

However, there’s one thing to keep in mind. The runtime code that Svelte generates for you isn’t as scalable once your app gets to a certain size.

You can check out this code size comparison on GitHub:

https://github.com/yyx990803/vue-svelte-size-analysis

Although Vue’s reactivity is VDOM-based, the Vue framework actually comes with quite a few compiler-based optimization processes. As a result, Vue doesn’t do a full VDOM diff on each data change. It would do the diffing only on the parts of the VDOM tree that depend on dynamic data. Internally, Vue also employs bitwise flags for DOM operations. So Vue’s DOM rendering speed is actually close to Svelte.

You can refer to this performance benchmark between various frameworks:

https://krausest.github.io/js-framework-benchmark/2021/table_chrome_94.0.4606.54.html


The Bottom Line

When we say Svelte is fast, it doesn’t mean Vue is slow. Frameworks like Vue and React are fast enough to deliver a pleasant user experience for most web projects (think 99.99% of them).

Svelte will shine only when you’re building something like a spreadsheet application where thousands of reactive values are interconnected. But most of us aren’t working on projects like that.

For the extra performance that Svelte gives you, there are some “sacrifices” you have to make.

As I’ve mentioned a few times previously, Svelte is not JavaScript. And that can be a problem if you’re stuck in a certain unique situation.

Let’s do a little experiment. If you paste the following Svelte code into a browser console, it will appear to run just fine:

let firstName = '';
let lastName = '';

$: fullName = firstName + ' ' + lastName;

function reset() {
  firstName = '';
  lastName = '';
}

But that’s only because strict mode is not turned on.

We can mimic a “strict mode” environment by wrapping the code in this IIFE (immediately invoked function expression):

(function(){
  'use strict'; // this bit turns on the strict mode

  let firstName = '';
  let lastName = '';

  $: fullName = firstName + ' ' + lastName;

  function reset() {
    firstName = '';
    lastName = '';
  }
})()

If you put the above snippet in a browser console, it will give you an error that says “fullName is not defined” because fullName isn’t properly declared with var, let, or const.

Svelte is a DSL (domain specific language) that looks like JavaScript, and it has to be compiled to be useful.

But, what’s the big deal? If it works in its own way, why does it matter that it isn’t JavaScript??

Most of the time, it doesn’t matter. But there are always rare situations like when you are stuck with one of those corporate computers at work, and you’re not allowed to install anything new on it, but you’re expected to develop a JavaScript app using only a code editor and a browser. In this case, you wouldn’t be able to use Svelte at all because you have to install a compiler before you can even develop Svelte.

In contrast, you can still use runtime-based frameworks like Vue and React.

Vue offers the option to develop apps without installing anything on your computer. You don’t need NPM, CLI tools, or Babel. All you need is a script tag with the CDN link in your HTML, and you can start developing.

<script src="https://unpkg.com/vue@next"></script>

So the bottom line is: Svelte is fast, but it’s not JavaScript so it won’t be able to run in a browser without a compilation step. On the other hand, runtime-based frameworks like Vue and React aren’t as fast as Svelte, but they come with more flexible options for setting up your development environment.

In practice, all of these rare scenarios are probably not relevant to most of us. You will find Vue fast enough for your projects, and you will be able to install Svelte on your computer just fine. So at the end of the day, it doesn’t really matter which framework you go with, as long as you enjoy using it and it suits you and/or your team’s needs.

With that being said, version 3 of Vue.js comes with the Composition API that allows you to use the same reactivity syntax (ref and reactive) outside of a component. To do the same thing in Svelte, you have to use Svelte store, which has a different reactivity syntax from Svelte component.


Looking Forward

Although Svelte’s hyper-performance perks aren’t enough to convert most developers, its compiler-oriented approach to implementing frontend frameworks breeds new possibilities in the frontend ecosystem. It isn’t set in stone that Vue can only be implemented runtime with virtual DOM. If extreme performance turns out to be a thing that can sway the public’s choice of framework, the Vue team could foreseeably come up with a compiler version of Vue that can convert the familiar Vue code to raw performant runtime code.

Download the cheatsheets

Save time and energy with our cheat sheets.