David Omotayo Frontend developer and indie game enthusiast.

Svelte vs. Vue: Comparing framework internals

12 min read 3539

Vue vs Svelte Comparing Framework Internals

There was a time when React, Angular, and Ember were at the forefront of web development, competing to be the best JavaScript framework, before Vue shot up in popularity, kicking Ember to the curb and taking its place.

Fast forward to 2022, and history seems to be repeating itself as Svelte, a relatively new framework, is gaining popularity at an exponential rate. We can’t help but ask ourselves if Svelte has what it takes to be a top contender.

Svelte seem to have taken a page out of Vue’s playbook by implementing and improving on the things that made us love Vue in the first place, like high performance, a lightweight and familiar templating syntax, and an easy learning curve.

In this article, we’ll examine the syntactical differences between Svelte and Vue, comparing how they work under the hood.

Table of contents

Fundamentals of Vue

Vue is a progressive, open source JavaScript framework for building user interfaces and single-page applications.

Vue was designed to be incrementally integrated, meaning you can add Vue to any part of an existing frontend project without having to rebuild the whole thing. Vue relies only on JavaScript, unlike frameworks like Svelte, which uses a compiler.

To build a single-page application with Vue, you’d have to use its CLI, a command-line utility tool for quickly scaffolding Vue boilerplate projects with different build systems.

At Vue’s core are a few concepts inherited from its competitors, Angular and React. For one, Vue utilizes Angular’s reactive, two-way data binding, which creates a reactive connection between the model and the view. Another is React’s Virtual DOM diffing, which prevents Vue from surgically updating the DOM every time something changes.

Partly inspired by the MVVM design pattern, Vue’s focus is on the view layer, or the template. However, every component instance in a Vue application is referred to as a ViewModel, or vm variable. The vm is the data-binding system that links the view and the model-view layers.

Fundamentals of Svelte

Svelte is an open source, frontend framework for creating interactive UIs. Unlike Vue, Svelte is a compiler that converts declarative state-driven components into imperative JavaScript codes that directly update the DOM.

We made a custom demo for .
No really. Click here to check it out.

With the Virtual DOM diffing technique, monolithic frameworks compile declarative codes at runtime. While this method is efficient and fast, it requires the browser to perform extra tasks before rendering a webpage, creating an overhead that affects the performance of the app.

By parsing and compiling its declarative codes into JavaScript codes that the browser can use during build time, Svelte avoids this performance overhead, making it 2x faster than frameworks that use the Virtual DOM and are compiled at runtime.

Getting started

Before we pit both frameworks against each other, let’s set up a sample app for each that we can reference throughout the article. First, create two new folders, one for each framework. Open your machine’s command line tool, cd into each folder, then run the commands below in their respective folders.

Vue

The command below will install the Vue CLI on your machine:

npm install -g @vue/cli
# OR
yarn global add @vue/cli

Once installation is complete, run the command below to scaffold a Vue project:

vue create vue-sample-app

To run the app, cd into the vue-sample-app folder:

cd vue-sample-app

Then, start the development server as follows:

npm run serve

Svelte

Open a new Svelte project with the command below:

npx degit sveltejs/template

Run the command below to install the required dependencies:

npm install

Now, start a new development server:

npm run dev

While we could create separate components for our app’s source codes and import them into the higher-level components in the project, App.svelte and App.vue, for simplicity, we’ll use the higher-level components instead.

Let’s clean up the App.svelte and App.vue components and place the following code blocks in their respective files:

//VUE

<template>
  <label>City </label>
  <input type="text" v-model="city"/>
  <label>Town </label>
  <input type="text" v-model="town"/>
  <div class="location">Location: {{location}} </div>
  <button v-on:click="handleReset">Reset</button>
</template>
<script>
export default {
  name: 'App',
  data(){
    return{
      city: "",
      town: ""
    }
  },
  methods: {
    handleReset(){
        this.city = ""
        this.town = ""
    }
  },
  computed: {
    location(){
      return this.city + " " + this.town
    }
  }
}
</script>
<style scoped>
#app {
  font-family: Avenir, Helvetica, Arial, sans-serif;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  text-align: center;
  color: #2c3e50;
  margin-top: 60px;
}
input{
  display: block;
  margin-bottom: 15px;
  width: 200px;
  height: 35px;
}
button{
  width: 60px;
  height: 30px;
}
.location{
  margin-bottom: 10px;
}
</style>
//SVELTE

<script>
    let city = "";
    let town = "";
    $: location = "Location: " + city + ' ' + town;
    const reset = () => {
        city = "";
        town = "";
    }
</script>
<main>
    <div>
        <label>City</label>
        <input type="text" bind:value={city}>
        <label>Town</label>
        <input type="text" bind:value={town}>
        <button on:click={reset}>Reset</button>
    </div>
    <div>
        {location}
    </div>
</main>
<style>
  main{
    background-color: white;
  }
</style>

Each app consists of two reactive variables, city and town, two input fields that are bound to the reactive variables, and a button with an event handler that resets the reactive variables when triggered.

Note: Because we’re using our apps for comparison purposes only, styling isn’t necessary.

Component structure and syntax comparison

You may have noticed that Svelte and Vue share a lot of features regarding templating structure and syntactical elements. Both use the single-file component, a special file format that encapsulates the template, logic, and styling of a component in a single .svelte or .vue file.

A single-file component is composed of three parts:

  • Template: The <template></template section contains the component’s markup in plain HTML
  • Script: The <script></script> section contains all the JavaScript logic within the component
  • Style: The <style></style> section contains a style that is locally scoped to the component

Svelte doesn’t have a dedicated template tag for encapsulating the markup in a component like Vue’s <template>. HTML markups can be defined directly inside the component and in any order.

Declarative rendering

The most noticeable difference between both templates is the method of referencing declarative data properties to the template. Svelte uses single curly braces { }:

<div>
  {location}
</div>

On the other hand, Vue uses double curly braces {{ }}, in what is also known as the mustache syntax:

<div>
  {{location}}
</div>

Data binding

To bind data from the model to the template, Svelte and Vue use special attributes called directives. A directive’s job is to reactively apply side effects to the DOM when the value of its expression changes.

bind is the directive for binding data in both Vue and Svelte. The bind directive accepts arguments denoted by a colon, as well as a data property or reactive variable that will be bound to the element it’s defined on.

The code below contains an input field element from our sample Svelte app. The bind directive is assigned a value argument and a reactive variable, city:

&lt;input type="text" bind:value={city}>

In the code above, the bind directive is binding the input field value to the reactive variable city. When the input field’s state changes, the reactive variable will update in correspondence to the change.

Vue adds a v- prefix to its directives, therefore, the bind directive will be defined in Vue as follows:

v-bind:value

In the Vue app, we switched up the v-bind directive with v-model. In Vue, v-bind binds data only one way, while v-model creates two-way bindings.

Now, suppose we use v-bind to bind our input fields to the data properties in the model. Only a change in the input field’s state would trigger reactivity. But with v-model, changes in both the data property and the input element’s state would trigger reactivity.

Computed property

Another peculiar syntax in our Svelte app is the dollar sign $: label, which is used to define a computed value by prefixing it to a top-level assignment, or assignments that are not inside a code block or function.

A computed value or property is a reactive declaration that accepts reactive variables as assignments. When reactive variables change, the computed value reacts. Basically, a computed value is a state that depends on other states.

Values that appear directly inside the $: block will become dependencies of the reactive statement.

In the case of our Svelte app, the location value is a computed value that depends on the reactive variables city and town. We assigned the reactive variables to the computed value and referenced the result in the template:

$: location = "Location: " + city + ' ' + town;

Whenever city or town changes, location will reactively re-compute and update the DOM in correspondence to the change.

In Vue, computed properties are defined in the component object and assigned named functions that return a code expression based on the data model:

export default {
  name: 'App',
  data(){
    return{
      city: "",
      town: ""
    }
  },
  computed: {
    location(){
      return this.city + " " + this.town
    }
  }
}

In this excerpt of our Vue sample app, we created a computed property and defined a location function in it. Then, we returned an expression that concatenates the city and town data properties in the function. Whenever these data properties change, the computed property will recompute and update its value in the DOM.

Note: The this keyword is used to reference the data properties.

Events

Both frameworks handle events in a similar way using the on directive, which accepts an argument denoted by a colon, just like the bind directive:

//Svelte
On:click
//Vue
v-on:click

An event handler directive traditionally evokes a function that returns an expression when triggered. In Svelte, we can define this function as we would any other function:

const reset = () => {
  city = "";
  town = "";
}

However, in Vue, we have to create a method property in the component model, just like we did for the computed property, and assign it a function that will return an expression when the event is triggered:

&lt;template>
  <button ="handleReset">Reset</button>
</template>

...

methods: {
  handleReset(){
      this.city = ""
      this.town = ""
  }
}

In the example above, we created a method property and defined a function that resets the reactive properties in the data function, city and town. Then, we assigned the function name handleReset to the event handler directive on the button.

In Vue, expressions and function names are defined or assigned to a directive with quotes:

<input type="text" v-model="city"/>

Like in most frameworks, functions and code expressions are assigned to directives in Svelte with curly braces { }:

<input type="text" bind:value={town}>

Reactivity

Reactivity, a style of programming that allows us to declaratively adjust to change, doesn’t work out of the box in JavaScript. If we were to store the sum of two variables and then reassign those variables, the original sum wouldn’t change:

let a = 10;
let b = 5;
let sum = a + b;
console.log(sum); // sum is 15
b = 15;
console.log(sum); // sum is still 15

To make something reactive in JavaScript, we have to detect when there’s a change in one of the values, track the function that changes it, and trigger the function so it can update the final value.

Most JavaScript frameworks offer special API functions that handle this logic under the hood. For example, to create reactive variables, Vue uses the data object in the option API and the ref() method in the Composition API. On the other hand, React uses the useState() Hook, and Angular uses the detectChanges() method.

Svelte, however, doesn’t use a special API to create reactivity. Reactive variables are created by default via assignments, and every variable declared with the let keyword is automatically reactive.

Let’s take a look at how Vue and Svelte handle reactivity during runtime and build time, respectively.

Build time reactivity

To understand how Svelte handles reactivity internally, we need to first understand how the Svelte compiler works.

When compiling Svelte components into imperative JavaScript, the compiler goes through a pipeline process, just as the virtual DOM does. The only difference is that the virtual DOM operates at run time, while the Svelte compiler operates at build time. Let’s review the entire process.

During compilation, Svelte implements a parser that is capable of parsing the HTML elements, logic blocks, and conditionals, but not the CSS content or JavaScript expressions in a component’s style and script tags.

Whenever the parser encounters the script tag or any code expressions inside of curly braces, it’ll hand over the operation to the Acorn JavaScript parser. Likewise, when the parser encounters the style tag, Svelte will hand over the operation to the CSS tree to handle the parsing.

Next, the compiler will break down the parsed code into smaller pieces called tokens. The compiler then creates a tree-like structure from the list of tokens called an abstract syntax tree (AST). An AST is a representation of the input code:

Abstract Syntax Tree Example

Based on the AST, the compiler will generate a code output. During the static analysis and renderer phases, the code output will be analyzed and used to generate the JavaScript code.

Svelte static analysis phase

To analyze the AST that was created, the compiler will create a component instance, which is a component class that stores the information of a Svelte component, like reactive variables, compile options, etc. This is known as the static analysis phase.

First, the component class will traverse the script AST and look for all the variables and functions being declared in the component. In our case, it will discover the city variable, the town variable, the reset function, and the location computed value.

Whenever any of these variables change, the component class will mark them as reassigned. Next, it will traverse the template AST and look up the variables and functions that were collected previously, marking them as references:

AST Reference Variables

Variables that aren’t referenced in the template don’t have to be reactive. Additionally, the elements where the variables are being referenced from in the template will keep the variables as dependencies. Therefore, whenever they change at runtime, the elements will need to be updated.

Finally, Svelte will traverse the CSS AST, updating the CSS selectors to be component scoped and warning for any unused selectors.

Svelte rendering phase

To generate an output code, Svelte will create a renderer instance based on a compile option. It will create either a DOM renderer for client-side rendering or an SSR renderer for server-side rendering.

Our sample app is not an SSR app, so Svelte will use the DOM renderer to generate the runtime code output:

Svelte DOM Renderer Runtime Code Output

To see the runtime code, open up your browser’s DevTool and navigate to the source tab, or the Debugger tab in Firefox. You should see a build folder in the sidebar. Inside that folder is a bundle.js file that houses all the runtime JavaScript code generated by the compiler.

If you look into the bundle.js file, you’ll notice it’s packed with code blocks spanning several lines. We’re interested in the $$instance code block, which is the function that wraps our app’s context:

function instance($$self, $$props, $$invalidate) {
    let location;
    let { $$slots: slots = {}, $$scope } = $$props;
    validate_slots('App', slots, []);
    let city = "";
    let town = "";

    const reset = () => {
      $$invalidate(0, city = "");
      $$invalidate(1, town = "");
    };

    const writable_props = [];

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

    function input0_input_handler() {
        city = this.value;
        $$invalidate(0, city);
    }

    function input1_input_handler() {
        town = this.value;
        $$invalidate(1, town);
    }

    $$self.$capture_state = () => ({ city, town, reset, location });

    $$self.$inject_state = $$props => {
        if ('city' in $$props) $$invalidate(0, city = $$props.city);
        if ('town' in $$props) $$invalidate(1, town = $$props.town);
        if ('location' in $$props) $$invalidate(2, location = $$props.location);
    };

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

    $$self.$$.update = () => {
        if ($$self.$$.dirty & /*city, town*/ 3) {
                $$invalidate(2, location = "Location: " + city + ' ' + town);
        }
    };

  return [city, town, location, reset, input0_input_handler, input1_input_handler];
}

The $$invalidate method is used to wrap the reactive assignments in different parts of the instance function, informing Svelte when the assignments change.

The $$invalidate method is an internal function that facilitates reactivity in Svelte. It marks every variable with a changed value as dirty, then it schedules an update. Svelte calls the update function to perform a check and determine which variable needs to be updated:

$$self.$$.update = () => {
    if ($$self.$$.dirty & /*city, town*/ 3) {
        $$invalidate(2, location = "Location: " + city + ' ' + town);
    }
};

The $$invalidate method is called inside the reset and input_handler functions, which are the parts of our app where changes occur. The former is the function we created in the app for resetting the reactive variables, while the latter is a function created by Svelte for binding the input field values to the reactive variables:

const reset = () => {
    $$invalidate(0, city = "");
    $$invalidate(1, town = "");
};

The input_handler functions were created by the bind directive that we assigned to the input fields in the template:

bind:value = {city}

The directive above will be compiled into the following code:

function input0_input_handler(){
    city = this.value; 
}

Vue runtime reactivity

Vue is made up of three core modules that facilitate the functionalities of the framework, the reactivity module, mount module, and the renderer module.

Reactivity module

The reactivity module allows us to create reactive JavaScript objects that we can watch for changes. We can track codes that depend on these objects when they are run, so they can be rerun later when the reactive objects change.

Mount module

The mount module compiles HTML templates into render functions. A render function can look something like the code below:

render() {
  return h(
    'h' + this.level, // tag name
    {}, // props/attributes
    this.$slots.default() // array of children
  )
}

Renderer module

The renderer module renders and updates the component onto a webpage. The process is split into three different phases.

In the render phase, the renderer module calls the render function, and it returns a virtual DOM node, or a VNode, which represents DOM elements with JavaScript objects. For example, HTML: <div>logRocket</div> can be represented by the VNode as follows:

{
  tag: "div",
  children: [
    {
    text: "logRocket"
    }
  ]
}

In the mount phase, the renderer takes the VNode and makes DOM JavaScript calls to create a webpage.

Finally, in the patch phase, the renderer takes the old and new VNodes, compares the two, and updates only the part of the webpage that has changed. The patch phase is triggered by the reactivity module when a reactive object changes.

Using the Virtual DOM, Vue ensures that these functionalities are performant across a large component tree.

To fulfill the core reactivity requirements that we stated earlier, the reactivity module will first create JavaScript objects based on the data properties present in the data function. Then, it will wrap the object in a proxy and store it as this.$data, meaning the data properties this.city and this.town in our sample app will become this.$data.city and this.$data.town. The former data properties are aliases of the latter.

Proxy is an ES6 JavaScript object that encases another object and allows you to intercept any interactions with that object. Next, Proxy will wrap the computed property location in an effect function that is run whenever the computed property is accessed. The effect will run the expressions inside the location function, for example, concatenating the city and town data properties.

During this operation, the proxy will invoke a handler function. Using the track function in the get handler inside the handler function, the handler function will track and record the effect that is currently running.

Vue knows to mark the data properties city and town as dep of the effect, meaning that location is dependent on city and town, therefore they are deps, dependencies, of location.

Finally, the module will create a set handler inside the handler function that will rerun the effect whenever the deps change. Inside the set handler is a trigger function that will look up the effects that depend on the properties, then initiate the re-run process.

Conclusion

Despite the perks that Svelte brings to the table, many developers just aren’t ready to migrate to it yet, mainly because of its lack of flexibility and small community support.

There is no doubt that Svelte is a performant framework, but Vue has had years and lots of support to implement and improve on these aspects. Svelte is a relatively new framework that’s still trying find its footings in the ecosystem.

Hopefully, the partnership between Rich Harris, the creator of Svelte, and Vercel will propel Svelte to the height it deserves. I hope you enjoyed this article!

 

Experience your Vue apps exactly how a user does

Debugging Vue.js applications can be difficult, especially when there are dozens, if not hundreds of mutations during a user session. If you’re interested in monitoring and tracking Vue mutations for all of your users in production, try LogRocket. https://logrocket.com/signup/

LogRocket is like a DVR for web and mobile apps, recording literally everything that happens in your Vue apps including network requests, JavaScript errors, performance problems, and much more. Instead of guessing why problems happen, you can aggregate and report on what state your application was in when an issue occurred.

The LogRocket Vuex plugin logs Vuex mutations to the LogRocket console, giving you context around what led to an error, and what state the application was in when an issue occurred.

Modernize how you debug your Vue apps - .

David Omotayo Frontend developer and indie game enthusiast.

2 Replies to “Svelte vs. Vue: Comparing framework internals”

Leave a Reply