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.
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.
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.
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.
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.
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
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.
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></template
section contains the component’s markup in plain HTML<script></script>
section contains all the JavaScript logic within the component<style></style>
section contains a style that is locally scoped to the componentSvelte 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.
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>
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
:
<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.
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.
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:
<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, 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.
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:
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.
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
:
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.
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:
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 is made up of three core modules that facilitate the functionalities of the framework, the reactivity module, mount module, and the renderer 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.
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 ) }
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.
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!
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.
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 — start monitoring for free.
Would you be interested in joining LogRocket's developer community?
Join LogRocket’s Content Advisory Board. You’ll help inform the type of content we create and get access to exclusive meetups, social accreditation, and swag.
Sign up nowMaking carousels can be time-consuming, but it doesn’t have to be. Learn how to use React Snap Carousel to simplify the process.
Consider using a React form library to mitigate the challenges of building and managing forms and surveys.
In this article, you’ll learn how to set up Hoppscotch and which APIs to test it with. Then we’ll discuss alternatives: OpenAPI DevTools and Postman.
Learn to migrate from react-native-camera to VisionCamera, manage permissions, optimize performance, and implement advanced features.
2 Replies to "Svelte vs. Vue: Comparing framework internals"
Excellent write-up and comparison! Thank you
your examples shows that Svelte is usually a lot less code to achieve the same thing