Compile Svelte in your Head

Part 2

Tan Li Hau
Mar 26 · 8 min read

In Part 1, I mentioned the $$invalidate function, I explained that the $$invalidate function works conceptually like the following:

But that’s not the exact implementation of the $$invaldiate function. So in this article, we are going to look at how $$invalidate is implemented in Svelte.

At the point of writing, Svelte is at v3.20.1.

Pre v3.16.0

There’s a big optimisation that changes the underlying implementation of the $$invalidate function in v3.16.0, namely in #3945. The underlying concept doesn't change, but it'll be much easier to understand about $$invalidate prior the change and learn about the optimisation change separately.

Let’s explain some of the variables that you are going to see, some of which was introduced in Part 1:

$$.ctx

There’s no official name for it. You can call it context as it is the context which the template is based on to render onto the DOM.

I called it instance variables. As it is a JavaScript Object that contains all the variables that you:

  • declared in the <script> tag
  • mutated or reassigned
  • referenced in the template

that belongs to a component instance.

The instance variables themselves can be of a primitive value, object, array or function.

The instance function creates and returns the ctx object.

Functions declared in the <script> tag will refer to the instance variable that is scoped withn the instance function closure:

Svelte REPL

Whenever a new instance of a component is created, the instance function is called and the ctx object is created and captured within a new closure scope.

$$.dirty

$$.dirty is a object that is used to track which instance variable had just changed and needs to be updated onto the DOM.

For example, in the following Svelte component:

Svelte REPL

The initial $$.dirty is null ( source code).

If you clicked on the “+ Agility” button, $$.dirty will turn into:

If you clicked on the “Level Up” button, $$.dirty will turn into:

$$.dirty is useful for Svelte, so that it doesn't update the DOM unnecessarily.

If you looked at the p (update) function of the compiled code, you will see Svelte checks whether a variable is marked in $$.dirty, before updating the DOM.

After Svelte updates the DOM, the $$.dirty is set back to null to indicate all changes has been applied onto the DOM.

$$invalidate

$$invalidate is the secret behind reactivity in Svelte.

Whenever a variable is

Svelte will wrap the assignment or update around with the $$invalidate function:

the $$invalidate function will:

  1. update the variable in $$.ctx
  2. mark the variable in $$.dirty
  3. schedule an update
  4. return the value of the assignment or update expression

Source code

One interesting note about the function $$invalidate is that, it wraps around the assignment or update expression and returns what the expression evaluates to.

This makes $$invalidate chainable:

It seemed complex when there’s a lot of assignment or update expressions in 1 statement! 🙈

The 2nd argument of $$invalidate is the assignment or update expressions verbatim. But if it contains any assignment or update sub-expressions, we recursively wrap it with $$invalidate.

In case where the assignment expression changes a property of an object, we pass the object in as a 3rd argument of the $$invalidate function, eg:

So that, we update the "obj" variable to obj instead of the value of the 2nd argument, "hello".

schedule_update

schedule_update schedules Svelte to update the DOM with the changes made thus far.

Svelte, at the point of writing ( v3.20.1), uses microtask queue to batch change updates. The actual DOM update happens in the next microtask, so that any synchronous $$invalidate operations that happen within the same task get batched into the next DOM update.

To schedule a next microtask, Svelte uses the Promise callback.

In flush, we call update for each component marked dirty:

Source code

So, if you write a Svelte component like this:

Svelte REPL

The DOM update for the givenName and familyName happens in the same microtask:

  1. Click on the “Update” to call the update function
  2. $$invalidate('givenName', givenName = 'Li Hau')
  3. Mark the variable givenName dirty, $$.dirty['givenName'] = true
  4. Schedule an update, schedule_update()
  5. Since it’s the first update in the call stack, push the flush function into the microtask queue
  6. $$invalidate('familyName', familyName = 'Tan')
  7. Mark the variable familyName dirty, $$.dirty['familyName'] = true
  8. Schedule an update, schedule_update()
  9. Since update_scheduled = true, do nothing.
  10. — End of task —
  11. — Start of microtask —
  12. flush() calls update() for each component marked dirty
  13. Calls $$.fragment.p($$.dirty, $$.ctx).
  • $$.dirty is now { givenName: true, familyName: true }
  • $$.ctx is now { givenName: 'Li Hau', familyName: 'Tan' }

14. In function p(dirty, ctx),

  • Update the 1st text node to $$.ctx['givenName'] if $$.dirty['givenName'] === true
  • Update the 2nd text node to $$.ctx['familyName'] if $$.dirty['familyName'] === true

15. Resets the $$.dirty to null

16. …

17. — End of microtask —

tl;dr

  • For each assignment or update, Svelte calls $$invalidate to update the variable in $$.ctx and mark the variable dirty in $$.dirty.
  • The acutal DOM update is batched into the next microtask queue.
  • To update the DOM for each component, the component $$.fragment.p($$.diry, $$.ctx) is called.
  • After the DOM update, the $$.dirty is reset to null.

v3.16.0

One big change in v3.16.0 is the PR #3945, namely bitmask-based change tracking.

Instead of marking the variable dirty using an object:

Svelte assign each variable an index:

and uses bitmask to store the dirty information:

which is far more compact than the previous compiled code.

Bitmask

For those who don’t understand, allow me to quickly explain what it is.

Of course, if you want to learn more about it, feel free to read a more detailed explanation, like this and this.

The most compact way of representing a group of true or false is to use bits. If the bit is 1 it is true and if it is 0 it is false.

A number can be represented in binary, 5 is 0b0101 in binary.

If 5 is represented in a 4-bit binary, then it can store 4 boolean values, with the 0th and 2nd bit as true and 1st and 3rd bit as false, (reading from the right to left, from least significant bit to the most significant bit).

How many boolean values can a number store?

That depends on the language, a 16-bit integer in Java can store 16 boolean values.

In JavaScript, numbers can are represented in 64 bits. However, when using bitwise operations on the number, JavaScript will treat the number as 32 bits.

To inspect or modify the boolean value stored in a number, we use bitwise operations.

The 2nd operand we use in the bitwise operation, is like a mask that allow us to target a specific bit in the 1st number, that stores our boolean values.

We call the mask, bitmask.

Bitmask in Svelte

As mentioned earlier, we assign each variable an index:

So instead of returning the instance variable as an JavaScript Object, we now return it as an JavaScript Array:

The variable is accessed via index, $$.ctx[index], instead of variable name:

The $$invalidate function works the same, except it takes in index instead of variable name:

$$.dirty now stores a list of numbers. Each number carries 31 boolean values, each boolean value indicates whether the variable of that index is dirty or not.

To set a variable as dirty, we use bitwise operation:

And to verify whether a variable is dirty, we use bitwise operation too!

With using bitmask, $$.dirty is now reset to [-1] instead of null.

Trivia: -1 is 0b1111_1111 in binary, where all the bits are 1.

Destructuring $$.dirty

One code-size optimisation that Svelte does is to always destructure the dirty array in the update function if there's less than 32 variables, since we will always access dirty[0] anyway:

tl;dr

  • The underlying mechanism for $$invalidate and schedule_update does not change
  • Using bitmask, the compiled code is much compact

Reactive Declaration

Svelte allow us to declare reactive values via the labeled statement, $:

Svelte REPL

If you look at the compiled output, you would find out that the declarative statements appeared in the instance function:

Try reorder the reactive declarations and observe the change in the compiled output:

Svelte REPL

Some observations:

  • When there are reactive declarations, Svelte defines a custom $$.update method.
  • $$.update is a no-op function by default. (See src/runtime/internal/Component.ts)
  • Svelte uses $$invalidate to update the value of a reactive variable too.
  • Svelte sorts the reactive declarations and statements, based on the dependency relationship between the declarations and statements
  • quadrupled depends on doubled, so quadrupled is evaluated and $$invalidated after doubled.

Since all reactive declarations and statements are grouped into the $$.update method, and also the fact that Svelte will sort the declarations and statements according to their dependency relationship, it is irrelevant of the location or the order you declared them.

The following component still works:

Svelte REPL

The next thing you may ask, when is $$.update being called?

Remember the update function that gets called in the flush function?

I put a NOTE: comment saying that it will be important later. Well, it is important now.

The $$.update function gets called in the same microtask with the DOM update, right before we called the $$.fragment.p() to update the DOM.

The implication of the above fact is

1. Execution of all reactive declarations and statements are batched

Just as how DOM updates are batched, reactive declarations and statements are batched too!

Svelte REPL

When update() get called,

  1. Similar to the flow described above, $$invalidate both "givenName" and "familyName", and schedules an update
  2. — End of task —
  3. — Start of microtask —
  4. flush() calls update() for each component marked dirty
  5. Runs $$.update()
  • As “givenName” and “familyName” has changed, evaluates and $$invalidate "name"
  • As “name” has changed, executes console.log('name', name);

6. Calls $$.fragment.p(...) to update the DOM.

As you can see, even though we’ve updated givenName and familyName, we only evaluate name and executes console.log('name', name) once instead of twice:

2. The value of reactive variable outside of reactive declarations and statements may not be up to date

Because the reactive declarations and statements are batched and executed in the next microtask, you can’t expect the value to be updated synchronously.

Svelte REPL

Instead, you have to refer the reactive variable in another reactive declaration or statement:

Sorting of reactive declarations and statements

Svelte tries to preserve the order of reactive declarations and statements as they are declared as much as possible.

However, if one reactive declaration or statement refers to a variable that was defined by another reactive declaration, then, it will be inserted after the latter reactive declaration:

Reactive variable that is not reactive

The Svelte compiler tracks all the variables declared in the <script> tag.

If all the variables of a reactive declaration or statement refers to, never gets mutated or reassigned, then the reactive declaration or statement will not be added into $$.update.

For example:

Svelte REPL

Since, count never gets mutated or reassigned, Svelte optimises the compiled output by not defining $$self.$$.update.

If you wish to know more, follow me on Twitter.

I’ll post it on Twitter when the next part is ready, where I’ll be covering logic blocks, slots, context, and many others.

⬅ ⬅ Previously in Part 1.

Originally published at https://lihautan.com

JavaScript in Plain English

Learn the web's most important programming language.

Tan Li Hau

Written by

Frontend Developer at Shopee

JavaScript in Plain English

Learn the web's most important programming language.

More From Medium

More from JavaScript in Plain English

More from JavaScript in Plain English

32 funny Code Comments that people actually wrote

10.2K

More from JavaScript in Plain English

More from JavaScript in Plain English

Welcome to a place where words matter. On Medium, smart voices and original ideas take center stage - with no ads in sight. Watch
Follow all the topics you care about, and we’ll deliver the best stories for you to your homepage and inbox. Explore
Get unlimited access to the best stories on Medium — and support writers while you’re at it. Just $5/month. Upgrade