· 8 min read

Deep Dive into Svelte (1) — The Svelte Compilation Process

This article was auto-translated from Chinese. Some nuances may be lost in translation.

Deep Dive into Svelte (1) — The Svelte Compilation Process

Before reading this article, it is assumed that readers already have experience using Svelte or other front-end frameworks and are interested in the underlying implementation.

If you haven’t read “How Svelte Compiles (0) — What is an Abstract Syntax Tree?”, it is recommended to read that first before continuing.

Today, this article aims to answer a few questions:

  • Why can Svelte compile code into JavaScript?
  • Why can we use template-engine-like syntax in Svelte (such as {#if}, {#await}, etc.), and how does it differ from conventional template engines?

Introduction

In order to generate the final code, Svelte must compile the component once to obtain the necessary information. The journey from Svelte’s compilation process to code generation mainly goes through several stages:

  • Parsing JavaScript, HTML + Svelte template syntax, and CSS into an AST
    • Parsing JavaScript (enclosed in <script> and {}) into an AST using acron
    • Parsing HTML + Svelte template syntax ({#if}, {variable}, etc.) into an AST using a custom parser
    • Parsing CSS into an AST using csstree
  • Calling new Component(ast) to generate the Svelte component, which mainly contains information such as instance, fragment, and vars (src/compiler/compile/Component.ts)
  • Calling renderer.render to generate js and css

The actual process and handling are much more complex than described above (intro and outro transitions, event listeners, variable tracking, etc.). You can refer to this diagram for the overall flow:

Svelte Compile flow

1. Parsing Source Code into an AST

Svelte first splits the component into three main parts: HTML (along with Svelte template syntax), CSS, and JavaScript, parsing each with different parsers.

If you want to see what a Svelte component looks like after being parsed, you can inspect it in AST Explorer:

<script>
  let count = 0;
  count++;
</script>

<style>
  p {
    font-size: 14px;
  }
</style>

<p>count is {count}</p>

The generated syntax tree (on the right side):

Screenshot_2021-02-07 AST explorer(3)

As you can see, after parsing, three ASTs are generated: html, css, and instance, where instance refers to the JavaScript code enclosed in <script>.

2. Generating the Svelte Component

svelte

At this stage, Svelte stores the essential information from the AST in the Component class, including the component’s HTML (named fragment in Svelte), declared variables, the instance AST, and so on.

Next, it traverses instance (the part enclosed in <script> in the diagram above) to track the usage of all variables. At this point, it can already detect whether a variable has been declared but not used, or whether variables prefixed with $ need special handling.

Then, it begins traversing the HTML portion and creates a fragment. This part can be considered one of the core logics in Svelte’s compilation. Fragments come in many types, including standard HTML tags and Svelte syntax like if, await, etc.

// https://github.com/sveltejs/svelte/blob/master/src/compiler/compile/nodes/shared/map_children.ts
function get_constructor(type) {
	switch (type) {
		case 'AwaitBlock': return AwaitBlock;
		case 'Body': return Body;
		case 'Comment': return Comment;
		case 'EachBlock': return EachBlock;
		case 'Element': return Element;
		case 'Head': return Head;
		case 'IfBlock': return IfBlock;
		case 'InlineComponent': return InlineComponent;
		case 'KeyBlock': return KeyBlock;
		case 'MustacheTag': return MustacheTag;
		case 'Options': return Options;
		case 'RawMustacheTag': return RawMustacheTag;
		case 'DebugTag': return DebugTag;
		case 'Slot': return Slot;
		case 'Text': return Text;
		case 'Title': return Title;
		case 'Window': return Window;
		default: throw new Error(`Not implemented: ${type}`);
	}
}

To facilitate handling, Svelte creates a dedicated class for each different type of fragment. We won’t go into detail on the implementation of every class here. Here are a few examples:

  • Element corresponds to regular HTML tags, handling things like event handlers, attribute checks, and accessibility (a11y) checks.
    • For example, it triggers a warning if tagName is a but lacks an href attribute (source code).
  • IfBlock handles {#if} and {:else} syntax.
  • EachBlock handles {#each} syntax.

After that, Svelte adds a hash to the corresponding CSS to prevent naming collisions, thereby generating the scoped css styles.

3. Creating Fragments and Blocks

Finally, we arrive at the code generation stage. The entire code generation logic resides in src/compiler/compile/render_dom/Renderer.ts (Svelte selects a renderer based on whether it is compiling for SSR or DOM; here we take DOM as an example).

First, it creates a fragment and defines the generated code content using the render function in the Wrapper. For example, Text.ts is responsible for handling text node generation. The fragment continuously traverses child nodes and calls the render function to produce the corresponding code, placing code fragments into a block.

Next, a block is declared. A block contains many code fragments (such as the code generated during mount and unmount), which will ultimately be used to build the create_fragment function. This part can be considered the most central and complex part of Svelte. You can check the complete implementation in src/compiler/render_dom/index.ts.

For the code generation part, it uses code-red, written by Svelte’s author Rich Harris, to facilitate generation. The unique feature of this library is that you can write something like var a = 1 to directly produce the corresponding AST node. For instance, variableA in an example would actually become a VariableDeclaration node. It also allows combining template literals for convenient code generation.

For code generation, you can refer to the iT Ironman Competition video — Generating Component Code

For example, if I want to dynamically generate an add function, I can write it like this:

Finally, the syntax tree is converted back into code via the print API. Let’s take a look at the implementation in Svelte (using EachBlock.ts as an example, since other generators are relatively complex) to see how the code generation source is written:

// 在元件 create 時呼叫 each_block_else.c()
block.chunks.create.push(b`
  if (${each_block_else}) {
    ${each_block_else}.c();
  }
`);

if (this.renderer.options.hydratable) {
  block.chunks.claim.push(b`
    if (${each_block_else}) {
      ${each_block_else}.l(${parent_nodes});
    }
  `);
}

// 在元件 mount 時呼叫 each_block_else.m()
block.chunks.mount.push(b`
  if (${each_block_else}) {
    ${each_block_else}.m(${initial_mount_node}, ${initial_anchor_node});
  }
`);

The b invoked inside is one of code-red’s APIs, and these are all snippets that will ultimately be generated in the final output.

Why can Svelte compile code into JavaScript?

Returning to the question posed at the beginning of this article: because Svelte pre-compiles and analyzes the code, it can compile .svelte files into JavaScript.

Why can we use template-engine-like syntax in Svelte, and how does it differ from conventional template engines?

Because Svelte implements a custom parser that, in addition to parsing standard HTML, can parse syntax inside {}, and then generates the corresponding JavaScript code through the compilation process mentioned above. The difference from traditional template engine syntax is that Svelte’s syntax is reactive, whereas typical template engines produce static HTML. Taking ERB as an example:

<% unless content.empty? %>
  <div>
    <%= content.text %>
  </div>
<% end %>

This kind of syntax is usually rendered into HTML on the backend before being sent back. In contrast, syntax in Svelte:

{#if content}
  <div>
    {content.text}  
  </div>
{/if}

will update the UI whenever content is not empty.

Conclusion

This article attempted to outline the general process of Svelte from compilation to code generation, including parsing code into an AST, constructing fragments and corresponding nodes, and finally generating code via the renderer. We didn’t dwell too much on the finer implementation details, which will be explored in depth in future articles. I hope that after reading this post, you have gained a solid understanding of how Svelte generates code.

References

Related Posts

Explore Other Topics