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.
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.
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
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
andlastName
- 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:
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
:
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:
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.
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:
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):
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:
- Vue will set up a dedicated object for each entry we provided through the
data
option. (this is implemented using ES5’sget/set
for Vue 2, and ES6’sproxy/reflect
for Vue 3). - These dedicated objects have the ability to invoke “handlers” whenever their respective data items are accessed or changed.
- 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 propertyfullName
, we are accessingfirstName
inside thefullName
computed property. SofullName
will be recorded in this case. Internally, this linkage is called a “dep”; it means thatfullName
is depending onfirstName
. - And whenever a data item such as
firstName
is changed (as in thereset
function), it will trigger a handler infirstName
’s dedicated object. The handler will make sure all the values that are depending onfirstName
also get updated to reflectfirstName
’s change. - 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.