Deep Understanding of Svelte (2) — Analyzing Svelte Generating Code

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.

Introduction

From the core principles of Svelte, it's clear that Svelte aims to extract necessary information during the compilation process to minimize dynamic overhead. The previous article explained how Svelte operates from compilation to code generation. Today, we'll take a closer look at how the generated code from Svelte works.

Let's first examine a simple Svelte component:

<script>
  import { onMount } from 'svelte';
  let count = 1;
  
  onMount(() => {
    setInterval(() => count++, 1000);
  })
</script>

{#if count != 100}
	<span>{count}</span>
{/if}

<p>
  this is text
</p>

The syntax for Svelte components is similar to standard HTML, with the addition of template-like syntax (such as if and await), making it largely compatible with HTML. However, the generated component is in JavaScript. For example, the above component compiles to:

// Generated by Svelte, with some code omitted
import { onMount } from "svelte";

function create_if_block(ctx) {
	let span;
	let t;

	return {
		c() {
			span = element("span");
			t = text(/*count*/ ctx[0]);
		},
		m(target, anchor) {
			insert(target, span, anchor);
			append(span, t);
		},
		p(ctx, dirty) {
			if (dirty & /*count*/ 1) set_data(t, /*count*/ ctx[0]);
		},
		d(detaching) {
			if (detaching) detach(span);
		}
	};
}

function create_fragment(ctx) {
	let t0;
	let p;
	let if_block = /*count*/ ctx[0] != 100 && create_if_block(ctx);

	return {
		c() {
			if (if_block) if_block.c();
			t0 = space();
			p = element("p");
			p.textContent = "this is text";
		},
		m(target, anchor) {
			if (if_block) if_block.m(target, anchor);
			insert(target, t0, anchor);
			insert(target, p, anchor);
		},
		p(ctx, [dirty]) {
			if (/*count*/ ctx[0] != 100) {
				if (if_block) {
					if_block.p(ctx, dirty);
				} else {
					if_block = create_if_block(ctx);
					if_block.c();
					if_block.m(t0.parentNode, t0);
				}
			} else if (if_block) {
				if_block.d(1);
				if_block = null;
			}
		},
		i: noop,
		o: noop,
		d(detaching) {
			if (if_block) if_block.d(detaching);
			if (detaching) detach(t0);
			if (detaching) detach(p);
		}
	};
}

function instance($$self, $$props, $$invalidate) {
	let count = 1;

	onMount(() => {
		setInterval(() => $$invalidate(0, count++, count), 1000);
	});

	return [count];
}

class App extends SvelteComponent {
	constructor(options) {
		super();
		init(this, options, instance, create_fragment, safe_not_equal, {});
	}
}

Svelte also supports SSR (Server-Side Rendering) functionality. If we compile the above code using Svelte's SSR feature, it will create a function that generates an HTML string.

// Generated by Svelte, with some code omitted
import { onMount } from "svelte";

const App = create_ssr_component(($$result, $$props, $$bindings, slots) => {
	let count = 1;

	onMount(() => {
		setInterval(() => count++, 1000);
	});

	return `${count != 100 ? `<span>${escape(count)}</span>` : ``}

<p>this is text
</p>`;
});

export default App;

Observing Generated Code (DOM)

For the sake of clarity, we will focus on the generated code related to DOM, skipping the SSR part for now.

The generated code primarily consists of three parts: the create_fragment function, the instance function, and the SvelteComponent class.

create_fragment

Let's first take a look at create_fragment:

function create_fragment(ctx) {
	let t0;
	let p;
	let if_block = /*count*/ ctx[0] != 100 && create_if_block(ctx);

	return {
		c() {
			if (if_block) if_block.c();
			t0 = space();
			p = element("p");
			p.textContent = "this is text";
		},
		m(target, anchor) {
			if (if_block) if_block.m(target, anchor);
			insert(target, t0, anchor);
			insert(target, p, anchor);
		},
		p(ctx, [dirty]) {
			if (/*count*/ ctx[0] != 100) {
				if (if_block) {
					if_block.p(ctx, dirty);
				} else {
					if_block = create_if_block(ctx);
					if_block.c();
					if_block.m(t0.parentNode, t0);
				}
			} else if (if_block) {
				if_block.d(1);
				if_block = null;
			}
		},
		i: noop,
		o: noop,
		d(detaching) {
			if (if_block) if_block.d(detaching);
			if (detaching) detach(t0);
			if (detaching) detach(p);
		}
	};
}

The create_fragment function returns an object containing several properties represented by single letters, which may seem ambiguous at first glance. However, these represent actions to be performed at different lifecycle stages:

  • c: Stands for create, the function to execute when the component is created.
  • m: Stands for mount, the function to execute when the component is mounted to the DOM.
  • p: Stands for patch, the function to execute when the component is updated.
  • i: Stands for intro, the function to execute during the component's transition into the DOM.
  • o: Stands for outro, the function to execute during the component's transition out of the DOM.
  • d: Stands for destroy or detach, the function to execute when the component is unmounted.

You can refer to the detailed source code and generation logic at src/compiler/compile/render_dom/Block.ts.

Understanding what each letter represents makes it much clearer what the code is doing:

  • It assigns the result of the condition (if count != 100) to if_block.
  • During create:
    • It creates the p element.
    • It sets p.textContent to this is text.
  • During mount:
    • If if_block is true, it calls if_block.m() (the mount action).
    • It inserts t0 into the anchor.
    • It inserts p into the anchor.
  • During patch:
    • If the condition count != 100 is true:
      • If if_block exists, it calls if_block.p().
      • If it doesn't exist, it calls create_if_block and executes if_block.m().
    • If the condition count != 100 is false:
      • This indicates that the contents of if_block should be removed, so it calls if_block.d().

instance

Next, let's look at the instance function:

function instance($$self, $$props, $$invalidate) {
	let count = 1;

	onMount(() => {
		setInterval(() => $$invalidate(0, count++, count), 1000);
	});

	return [count];
}

The code inside the <script> tag is encapsulated within the instance function. There are a few notable points here:

  • The original code setInterval(() => count++, 1000) is transformed into setInterval(() => $$invalidate(0, count++, count), 1000) after compilation.
  • The return value is an array that returns the value of count.

Svelte performs static analysis to gather information about variables, enabling it to handle dependency tracking. Here, $$invalidate functions similarly to setState in React. The key difference is that one requires manual invocation while Svelte automates detection and handling.

The implementation of $$invalidate looks like this (with some code omitted):

// Marks the component as dirty (indicating it needs to be updated) if it detects a change in the variable value
if ($$.ctx && not_equal($$.ctx[i], $$.ctx[i] = value)) {
  if (!$$.skip_bound && $$.bound[i]) $$.bound[i](value);
  if (ready) make_dirty(component, i);
}

function make_dirty(component, i) {
	if (component.$$.dirty[0] === -1) {
		dirty_components.push(component);
		schedule_update();
		component.$$.dirty.fill(0);
	}
	component.$$.dirty[(i / 31) | 0] |= (1 << (i % 31));
}

Every time setInterval triggers count++, $$invalidate is called. It first compares the previous and current values. If there's an update, it invokes the mark_dirty function and adds the component to dirty_components, scheduling an update. Svelte also implements a batch update mechanism that aims to perform as many updates as possible in a single frame.

SvelteComponent

class App extends SvelteComponent {
	constructor(options) {
		super();
		init(this, options, instance, create_fragment, safe_not_equal, {});
	}
}

The SvelteComponent implementation is quite straightforward. It calls the init function, which is primarily responsible for initializing the Svelte component and invoking create_fragment and instance, allowing the component to be mounted to the DOM.

Summary

The code generated by Svelte generally comprises three main components: create_fragment, instance, and SvelteComponent.

  • create_fragment: Instructs Svelte on how to handle each lifecycle phase of the component.
  • instance: Executes the code within the <script> tag and returns the context (props, variables, etc.).
  • SvelteComponent: Initializes the Svelte component through the init function.

This article has elucidated how to interpret the code generated by Svelte and provided a basic overview of the underlying reactive mechanisms at play (although many mechanisms are not covered here and will be discussed in future articles). With this understanding, readers should now be able to comprehend the code produced by Svelte!

For more articles related to Svelte, you can refer to this link.

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

Buy me a coffee