In-depth understanding of Svelte (1) — Svelte Compilation Process

Written byKalanKalan
💡

If you have any questions or feedback, pleasefill out this form

This post is translated by ChatGPT and originally written in Mandarin, so there may be some inaccuracies or mistakes.

Understanding Svelte Deeply (1) — The Svelte Compilation Process

Before reading this article, it's expected that the readers have some experience with Svelte or other frontend frameworks and are interested in the implementation principles.

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

In today's article, we aim to answer several questions:

  • Why can Svelte compile code into JavaScript?
  • Why can Svelte use syntax similar to template engines (such as {#if} and {#await}), and how does it differ from general template engine syntax?

Introduction

To generate the final code, Svelte must compile components to gather the necessary information. The compilation process in Svelte involves several main stages:

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

The actual processing is much more complex than described above (including intro/outro control, event listening, variable tracking, etc.). You can refer to this diagram for a complete view:

Svelte Compile flow

1. Parsing Source Code into AST

Svelte begins by breaking down components into three main parts: HTML (along with Svelte's syntax), CSS, and JavaScript, each parsed by different parsers.

If you want to see what a Svelte component looks like after parsing, you can check it out in AST Explorer:

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

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

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

The generated syntax tree (right side):

Screenshot_2021-02-07 AST explorer(3)

You can see that after parsing, three ASTs are generated: html, css, and instance, where instance refers to the JavaScript code contained within <script>.

2. Generating Svelte Component (Component)

svelte

At this stage, Svelte stores necessary information from the AST in a Component class, which includes the component's HTML (referred to as fragment in Svelte), declared variables, the instance's AST, and more.

Next, it traverses the instance (the part enclosed in <script> in the image above) to determine the usage of all variables. At this point, it can already detect whether variables are declared but unused, and if there are any variables prefixed with $ that need special handling.

Then, it starts traversing the HTML part and creates a fragment. This part can be considered one of the core logics in Svelte's compilation. Fragments can be of various types, including standard HTML tags and Svelte-specific syntax like if and await.

// 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}`);
	}
}

For each different type of fragment, Svelte creates a corresponding class for easier management. Here are a few examples:

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

Then, Svelte adds a hash to the corresponding CSS to prevent naming conflicts, ultimately generating the css styles.

3. Creating Fragments and Blocks

Finally, we reach the code generation stage. The entire logic for generating code is found in src/compiler/render_dom/Renderer.ts (Svelte chooses the renderer based on whether it’s SSR or DOM; we'll use DOM as an example).

First, a fragment is created, and the content for code generation is defined using the render function within Wrapper. For instance, Text.ts is responsible for handling text generation. The fragment continuously traverses child nodes and calls the render function to produce the corresponding code, placing code snippets into blocks.

Next, blocks are declared, which contain numerous code fragments (e.g., code generated during mount and unmount). Ultimately, this is used to create the create_fragment function. This part can be considered the most complex and core aspect of Svelte. You can view the full implementation in src/compiler/render_dom/index.ts.

The code generation utilizes a library called code-red, written by author Rich Harris, to facilitate generation. This library's unique feature allows you to directly produce corresponding AST nodes with syntax like var a = 1. For example, in the case of variableA, it will actually become a VariableDeclaration node. You can also use template literal syntax to easily assemble code.

For more about the code generation process, refer to the IT Ironman competition video — Generating Component Code.

https://youtu.be/lxd6vsmL7RY

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

Finally, the syntax tree is converted into code through the print API. Let's explore the implementation in Svelte (using EachBlock.ts as an example, as others are relatively more complex) and see how the generated code is written:

// Call each_block_else.c() when the component is created
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});
    }
  `);
}

// Call each_block_else.m() when the component is mounted
block.chunks.mount.push(b`
  if (${each_block_else}) {
    ${each_block_else}.m(${initial_mount_node}, ${initial_anchor_node});
  }
`);

Here, the b called is one of the APIs from code-red; these are the codes that will eventually be generated.

Why Can Svelte Compile Code into JavaScript?

Returning to the questions at the beginning of this article, Svelte can compile and analyze the code in advance, allowing it to transform .svelte files into JavaScript.

Why Can Svelte Use Syntax Similar to Template Engines, and How Does It Differ from General Template Engine Syntax?

Svelte has implemented a customized parser that not only analyzes standard HTML but also processes the syntax inside {}, generating the corresponding JavaScript code through the aforementioned compilation process. The difference from standard template engine syntax is that Svelte's syntax is reactive, while typical template engine syntax is static HTML. For example, in ERB:

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

This syntax is typically rendered on the backend and returns HTML after processing. However, the syntax in Svelte:

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

will update the display when content is not empty.

Conclusion

This article attempts to outline the general process of Svelte from compilation to code generation, including parsing code into AST, creating fragments and corresponding nodes, and finally generating code through the renderer. The focus was not on the details and implementations, which will be explored in future articles. I hope readers can gain a solid understanding of the process by which Svelte generates code after finishing this article.

References

If you found this article helpful, please consider buying me a coffee ☕ It'll make my ordinary day shine ✨

Buy me a coffee