DEV Community

Joshua Nussbaum
Joshua Nussbaum

Posted on • Updated on

The Svelte compiler: How it works

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)
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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;
}
Enter fullscreen mode Exit fullscreen mode

Turns into:

h1.random-code-abc123 {
  color: teal;
}
Enter fullscreen mode Exit fullscreen mode

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}
Enter fullscreen mode Exit fullscreen mode

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
    }
  }
})
Enter fullscreen mode Exit fullscreen mode

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)
})
Enter fullscreen mode Exit fullscreen mode

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: ...
    }
  ] 
}
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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)
    }
  };
}
Enter fullscreen mode Exit fullscreen mode

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()
Enter fullscreen mode Exit fullscreen mode

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

Oldest comments (12)

Collapse
 
opensas profile image
opensas

Excellent article, it's the most simple explanation I found about Svelte magic!

Collapse
 
fliponeup profile image
Oscar Lito M Pablo

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.

Collapse
 
sno2 profile image
Carter Snook

Well, looks like I have a lot more time to put into to try to recreate something like this for Deno 😅

Collapse
 
joshnuss profile image
Joshua Nussbaum

Go for it!

Reverse engineering is one of the best ways to learn things.

Collapse
 
sno2 profile image
Carter Snook • Edited

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!

Thread Thread
 
realfakenerd profile image
Lucas A. Ouverney • Edited

Don't let your dreams be dreams, what are you waiting for? Just do it!

Collapse
 
riviergrullon profile image
Rivier Grullon
Collapse
 
the_one profile image
Roland Doda

OMG that is a GOLD article! I was looking on how Svelte does it's magic and this definitely explains so much! THANK YOU

Collapse
 
jamesyoung1337 profile image
James Young

Great post! Love it.

Collapse
 
oxharris profile image
Oxford Harrison

Awesome.

Collapse
 
anshumeena1947 profile image
Anshu Meena

wow! its really awesome thanks for this amazing article

Collapse
 
k_penguin_sato profile image
K-Sato • Edited

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!!