Want to learn more about Svelte? I just launched a few short video courses ✨
Most front-end frameworks rely on a diffing engine that syncs the visual DOM with an in-memory copy of the DOM.
Svelte is different. It's a compiler. It generates code (JavaScript) that updates the visual tree directly, without diffing.
Think of it as converting html like <h1>Hello World</h1>
into:
const element = document.createElement('h1')
element.textContent = "Hello World"
document.body.appendChild(element)
Now, why would you want to do that?
Because of data binding.
It means we can write <h1>{someValue}</h1>
declaratively and and we don't need to write imperative statements like element.textContent = someValue
every time someValue
changes. Svelte generates the synchronization code for us.
How the compiler works
The compiler takes in .svelte
files, parses them into an AST Abstract Syntax Tree, analyzes the tree, and generates Javascript and CSS.
Disclaimer: the examples below are simplified for brevity.
Parsing tags
Recall that the anatomy of a .svelte
file is similar to a .html
file:
<script>// js goes here</script>
<style>/* css goes here */<style>
<!-- More (visual) html tags here -->
<h1>...</h1>
<p>...</p>
The first step is to parse the document and create 3 buckets for tags: <script>
tags, <style>
tags, and visual tags (everything else).
Parsing CSS
The <style>
tags are parsed out so that we can add a unique prefix to each CSS rule.
For example:
h1 {
color: teal;
}
Turns into:
h1.random-code-abc123 {
color: teal;
}
The unique prefix is added to avoid clashing with CSS rules defined in other components.
The package css-tree is used to walk the CSS and inspect the expressions.
import {parse, walk, generate} from 'css-tree'
// parse CSS source to AST
const input = '.example { color: teal }'
const ast = parse(input)
const randomPrefix = 'xyz123'
const selectors = []
// traverse AST and looking for selectors
walk(ast, node => {
// check if this node is a selector
if (node.type === 'Selector') {
// capture this node, so we can modify it later
selectors.push(node)
}
})
// modify the AST
selectors.forEach(selector => {
// add a `ClassSelector` with name `.xyz123`
// it will turn `.example` into `.example.xyz123`
selector.children.insertData({
type: 'ClassSelector',
name: randomPrefix
})
})
// generate CSS text from AST
const output = generate(ast)
// print the CSS text
console.log(output)
//> .example.xyz1234{color:teal}
Parsing JavaScript
Svelte parses the <script>
tags to extract exports
statements (which are props) and to find reactive statements.
The JavaScript source code is turned into AST using the acorn package.
For example, say you define a prop export let name
. All the export let
statements can be located by walking the AST with estree-walker:
import {parse} from 'acorn'
import {walk} from 'estree-walker'
// define source code with 2 exported props
const sourceCode = "export let title, color"
// parse the source code
// enable `sourceType: 'module'` since want to allow exports
const ast = parse(sourceCode, {sourceType: 'module'})
// walk the AST
walk(ast, {
enter(node) {
// check if this node is a "named export"
if (node.type === 'ExportNamedDeclaration') {
// named exports can have many names, so map the ids
const props = node.declaration.declarations.map(declaration => declaration.id.name)
// print 'em
console.log(`We got props: ${props.join(', ')}`)
//> We got props: title, color
}
}
})
Parsing visual tags
The remaining tags are visual tags like <h1>
, <p>
, etc..
Svelte uses it's own tag parser, but you can use parse5 to do the same job:
import { parseFragment } from 'parse5'
const source = "<h1 class='snazzy'>Hello World!</h1>"
const fragment = parseFragment(source)
fragment.childNodes.forEach(node => {
console.log(node)
})
It outputs:
{
nodeName: 'h1',
tagName: 'h1',
attrs: [ { name: 'class', value: 'snazzy' } ],
namespaceURI: 'http://www.w3.org/1999/xhtml',
childNodes: [
{
nodeName: '#text',
value: 'Hello World!',
parentNode: ...
}
]
}
This gives us the complete tree of our HTML document, which we'll use to generate the equivalent JavaScript code.
Putting it all together
Say we have a simple .svelte
file like this:
<script>
export let name;
function handleClick(e) {
e.preventDefault()
alert(`Hello ${name}!`)
}
</script>
<h1 class="snazzy" on:click=handleClick>Hello {name}!</h1>
The compiler generate a .js
that looks like this:
// target: this is the target element to mount the component
// props: a list of props, defined with `export let`
export default function component({ target, props }) {
// code generated to extract the props into variables:
let { name } = props;
// all functions are copied directly from the <script> tag
function handleClick(e) {
e.preventDefault();
alert(`Hello ${name}!`);
}
// variables are declared for each element and text node:
let e0, t1, b2, t3;
// returns an object with lifecycle functions to create, mount, detach and update the component.
return {
// called when the components is created
// creates elements/nodes, adds attributes and wires up event handlers
create() {
e0 = document.createElement("h1")
t1 = document.createTextNode("Hello ")
b2 = document.createTextNode(name)
t3 = document.createTextNode("!")
e0.setAttribute("class", "snazzy")
e0.addEventListener("click", handleClick)
},
// called when the component is mounted to the `target`
// it just appends things
mount() {
e0.appendChild(t1)
e0.appendChild(b2)
e0.appendChild(t3)
target.append(e0)
},
// called to change the value of props
update(changes) {
// check if name changed
if (changes.name) {
// update `name` variable and all binding to `name`
b2.data = name = changes.name
}
},
// called to remove the component from the DOM
detach() {
e0.removeEventListener("click", handleClick)
target.removeChild(e0)
}
};
}
Now we can mount this component into the DOM:
import MyComponent from './component'
// instantiate the component
const component = MyComponent({
target: document.body,
props: {name: "World"}
})
// create the nodes
component.create()
// append the nodes into the target
component.mount()
Summary
Svelte is a compiler that parses .svelte
files, analyzes them and then generates a JavaScript file. The JavaScript file contains the logic to mount the component, handle events, and patch the DOM when values change.
For learning sake, I built a tiny version of the compiler: https://github.com/joshnuss/micro-svelte-compiler
It only does a fraction of the real compiler, but it's a useful learning tool.
TIP #1: If you want to see further examples, take a look at the JS Tab
in the Svelte REPL.
TIP #2: AST Explorer is a great learning tool for inspecting AST.
Happy coding!
✌
Want more?
If you'd like to learn more about Svelte, check out my short video courses ✨
Top comments (12)
Thank you for this!! The official doc didn't really explain how it actually works in depth. (Or I simply missed it)
This was extremely informative!!
Joshua, you may want to put your Svelte course on Udemy.com so you'll have a potentially-huge customer base and solves practicallly all your promotion challenges. Doing so also will help Svelte's exposure to more developers worldwide.
Well, looks like I have a lot more time to put into to try to recreate something like this for Deno 😅
github.com/crewdevio/Snel 👀
Go for it!
Reverse engineering is one of the best ways to learn things.
Well, after spending quite some time working on implementing this, I am probably going to first make this as a ssg with a couple of directives, then try to implement the same design fundamentals as my current one with. The compilation of the .svelte to .js for actual dom manipulation is very hard and doing it with the current module ecosystem that deno is quit an enigma to me 😂 Thanks for your encouraging words, though!
Don't let your dreams be dreams, what are you waiting for? Just do it!
Excellent article, it's the most simple explanation I found about Svelte magic!
OMG that is a GOLD article! I was looking on how Svelte does it's magic and this definitely explains so much! THANK YOU
Great post! Love it.
Awesome.
wow! its really awesome thanks for this amazing article