Svelte is truly a new and revolutionary approach to web development! It does an amazing job of automating your app's reactivity.
This article is focused on uncovering some "behind the scene" detail of Svelte's reactivity. We will do this from an "observer perspective" - by visualizing Svelte's reactivity first-hand (right in our application)! There's a bit of a mystery behind some of these concepts. We will delve into some lesser-known detail (that you may have not considered)! The hope is you will come away with more insight, and be better informed on how to use this awesome product!
This is not a Svelte tutorial. For the most part, any developer should be able to comprehend the concepts discussed here. Ideally you should know the basics of Svelte. While you won't need to be an expert, we will not spend time explaining rudimentary Svelte constructs.
A big thanks to Mark Volkmann for his review of my effort. I am just starting my Svelte journey, so Mark's insight was invaluable! You may want to check out his new book: Svelte and Sapper in Action.
At a Glance
- TL;DR
- Video Presentation
- Svelte Reactivity
- Reactive Triggers
- Exploring App Reactivity
- Demo App
- Inspecting App Reactivity
- Re-Render Analysis
- App Reactivity Tweaks
- Extra Credit Exercise
- Who is this guy?
- Summary
TL;DR
As a general rule, I would recommend reading the article in it's entirety. With that said, if you wish to "cut to the chase", I have summarized my findings at the end ... feel free to jump ahead to the Summary!
Video Presentation
If you are visually inclined, I gave the following presentation to Svelte Summit in the fall of 2021:
Full Syllabus & Resource Links
Svelte Reactivity
Being new to Svelte, I am absolutely fascinated with the fact that "reactivity is simply baked right into my app"! This is sooooo kooool!
In frameworks like React, your app must trigger reactivity (with things like setState()
), and the big focal point is: "When does my component re-render"?
With Svelte, reactivity happens automatically, and it is much finer grained than the component level. In Svelte, individual snippets of any component may dynamically re-generate at any time!
Svelte meticulously manages each snippet, monitoring and re-executing them as needed, based on changes in their dependent state. Herein lies the beauty of Svelte: it accomplishes all of this automatically through it's compiler philosophy ... converting our declarative component-based code into JavaScript that incrementally manipulates the DOM directly! Svelte both eliminates boilerplate code, and is truly reactive out-of-the-box, without employing a bloated in-memory run-time framework. How kool is that?
So what (you may be asking) is a snippet? This is actually my term. For the purposes of this article, the term "snippet" refers to any JavaScript expression that Svelte reactively manages and re-executes at the appropriate time (i.e. whenever dependent state changes). Ultimately, snippets are used in providing the dynamics of our html markup (i.e. it's reactivity).
Snippets may be found in two places:
-
code-snippets:
code-snippets are found in the component's JavaScript code (within the<script>
tag), and demarked with Svelte's$:
label. This is referred to as Reactive Declarations and Reactive Statements.
$: {name, phone} = user;
Generally speaking, code-snippets are typically lightweight, in that they result in changes to JavaScript state variables. With that said, however, the sole reason for these state changes is to be referenced in our html markup (either directly or indirectly).
In the sample above, the snippet will re-execute whenever the
user
object changes (the snippet's dependency), re-assigning thename
andphone
variables. -
html-snippets:
html-snippets are found in the component's html markup, delineated through curly braces:{...}
. This is commonly referred to as interpolation.
<p>Hello {name}</p> <p>May we call you at {phone}</p>
html-snippets are typically more heavyweight, in that they result in changes to the html DOM! But hey ... that is the whole reason we are here ... i.e. the crux of our reactivity!
In the sample above, the first snippet will re-generate it's html when
name
changes, and the second whenphone
changes.
Terminology: snippet, code-snippet, and html-snippet
Within this article, the term snippet refers to any JavaScript expression that Svelte reactively manages and invokes when it's dependent state changes.
Snippets may either be:
- code-snippets (referred to as Reactive Declarations and Reactive Statements)
- or html-snippets (commonly referred to as interpolation).
Ultimately, snippets provide the dynamics within our html markup (i.e. it's reactivity).
Reactive Triggers
So drilling into this topic just a bit more, you may be asking: How does Svelte determine when to trigger the re-execution our snippets?
The short answer is that Svelte monitors the dependent state referenced in each snippet, and triggers a re-execution whenever that state changes.
The followup question is: How does Svelte determine that state references have changed?
The Svelte docs talk about "Assignments are 'reactive'" and "Svelte's reactivity is triggered by assignment". What they are saying is that Svelte triggers reactivity through assignment semantics (recognizing assignment in it's various forms).
This is true for the local state of a component. The Svelte compiler will recognize assignment (in it's various forms), and mark the assigned variable as changed (i.e. "stale").
However, I have discovered that there is a big distinction in whether the assignment target is a primitive or an object (including arrays).
Primitive Types
For primitive types (strings, numbers, booleans, etc.), reactivity only occurs when the value has changed. In other words, it also incorporates JavaScript identity semantics (i.e. priorState === nextState
).
So myNum = (x + y) / 2
will only be marked as "stale" when it's value actually changes. If the prior value was 10 and the calculation resulted in 10, then no reactivity will occur.
This is most certainly what one would expect, and I suppose it is rather obvious. However the Svelte docs do not mention this (as far as I can tell). The only reason I dwell on this point is that it is in stark contrast to object-based reactivity.
Object Types
Most applications require more complex state, typically modeled in objects (or arrays).
As it turns out, in Svelte, any technique by which you change an object, will mark the entire object as "stale". This includes local component objects, Svelte object stores, component object properties, etc. This is also true when you mutate an object, and inform Svelte that the object has changed (by assigning it to itself).
This means that the granularity of change that is being tracked is much broader in scope. In other words, the entire object will be considered "stale" even though only one property may have changed.
Insight: Reactivity is based on change in dependent state
Snippet execution is triggered when dependent state changes
primitive types trigger reactivity only when the value changes (based on identity semantics)
objects trigger reactivity per the entire object, not individual content
Staleness Summary
The following table highlights what Svelte will consider "stale":
Given:
let prim = 1945; // a primitive
let myObj = { // an object
foo: 1945,
bar: 1945,
};
Operation Marks this as "stale"
============================= ======================================
prim++ prim
prim = 1945 prim (ONLY if prior value is NOT 1945)
myObj.foo++ myObj (all content of myObj)
myObj = {...myObj, foo:1945} myObj (all content of myObj)
myObj.foo = 1945 myObj (all content of myObj)
myObj = myObj myObj (all content of myObj)
incrementFooIndirectly(myObj) NOTHING
You can see a demo of this in my Reactive Triggers REPL. This visualizes reflexive counts (highlighted in yellow), from the various operations (listed above). In order to fully understand how this REPL works, you need to know what a ReflectiveCounter
is (a new tool for your tool belt). This is discussed in the Advanced Diagnostics section. You may want to come back to this REPL after reading the next section.
Exploring App Reactivity
Being the curious fellow that I am, I want to see my app's reactivity. After all, I am originally from Missouri - the "Show-Me" State!
You might be saying: "of course you can see the reactive results of your production app, through the state it is visualizing"!
BUT NO ... that is NOT what I am talking about. I want to definitively determine when Svelte triggers the execution of my snippets! In other words, I want to see Svelte's reactivity in action!
In so doing, this will:
help ground me in the Svelte philosophy
give me insight on various Svelte heuristics (dependency monitoring, reactive triggers, DOM updates, etc.)
give me a better appreciation for "all this reactivity that is occurring (automatically) all around me"
and we may just discover some detail that we hadn't considered!
Of course, this is something that would be confined to a category of "diagnostic probes", and not part of our production app.
At first glance, this seems like a "difficult task", because Svelte is in control of this (not us). And Svelte Devtools doesn't provide any insight on this either (it's focus is on reviewing state at a given point-in-time).
Diagnostic Logging Probes
As it turn out, we can use a common "developer trick" to logically-OR a console.log() prefix to each of our snippets.
Consider this:
Original:
<p>Hello {name}</p>
<p>May we call you at {phone}</p>
With Logging Probes:
<p>Hello {console.log('Name section fired) || name}</p>
<p>May we call you at {console.log('Phone section fired) || phone}</p>
We have now prefixed each production expression with a console.log()
that is logically-ORed. Because console.log()
returns nothing (i.e. undefined
which is falsy), the subsequent expression will unconditionally execute (resulting in the original html output).
In other words, this will generate the same html (as our original production code), but with the addition of diagnostic logs that are emitted only when the snippet is executed.
As an example, say our phone
state changes ... we will see the following emitted in our logs:
logs:
Phone section fired
You can see a live demo of this in the Logging Probes discussion.
It is important to use unique texts in each probe, so as to be able to correlate each log entry to it's corresponding snippet.
With the addition of these diagnostic probes, our logs will definitively reveal when Svelte re-executes each snippet ... very kool indeed!
Takeaway: Monitor Svelte snippet invocations through logically-ORed prefixed expressions
You can detect when Svelte executes a snippet by prefixing it with a logically-ORed monitor.
This can be a simple logging probe or a more advanced ReflectiveCounter
Advanced Diagnostics
For most applications, these simple diagnostic logging probes will provide adequate insight into your app's reflexiveness.
However, depending on how many probes you need, it may become tedious to correlate these logs to the various sections.
In these cases we can replace the logs with a simple monitor, that exposes a reflective-count for each section, displayed directly on our page!
Here is the utility:
createReflectiveCounters.js
export default function createReflectiveCounter(logMsg) {
// our base writable store
// ... -1 accounts for our initial monitor reflection (bumping it to 0)
const {subscribe, set, update} = writable(-1);
// expose our newly created custom store
return {
subscribe,
monitor(...monitorDependents) {
update((count) => count + 1); // increment our count
logMsg && console.log(logMsg); // optionally log (when msg supplied)
return ''; // prevent rendering `undefined` on page (when used in isolation)
// ... still `falsy` when logically-ORed
},
reset: () => set(0)
};
}
This creates a ReflectiveCounter
(a custom store), suitable to be used in monitoring Svelte reflective counts.
In it's rudimentary form, a ReflectiveCounter
is just a simple counter, however it's API is tailored to be used as a reflective monitor.
The monitor()
method should be prefixed in a "Svelte invoked" snippet (either through a logically-ORed expression, or a JS comma operator). It maintains a count of how often Svelte executes this snippet.
Example:
<i>{fooSectionReflexiveCount.monitor() || $foo}</i>
In turn these counts can be summarized directly on your page!
Example:
<mark>{$fooSectionReflexiveCount}:</mark>
The monitor()
method may also be optionally supplied a set of monitorDependent
parameters. This is used when the dependents you wish to monitor are NOT already part of the production snippet. Technically the utility does not use these parameters, rather it merely informs Svelte to monitor these dependents as criteria for re-invoking the snippet. The following example monitors how many times a Svelte store has changed:
Example:
$: fooStateChangeCount.monitor($foo);
You may also optionally console log a message, whenever the monitor() is executed, by supplying a logMsg
to the creator:
Example:
const fooSectionReflexiveCount = createReflectiveCounter('Foo section fired');
The reset()
method can be used to reset the given count.
USAGE:
There are two distinct ways that ReflectiveCounter
can be used:
-
Monitor html reflexive counts (in html-snippets):
<script> const fooReflexiveCount = createReflectiveCounter('foo section fired'); </script> <!-- diagnostic reporter --> <mark>{$fooReflexiveCount}:</mark> <!-- monitor this section --> <i>{fooReflexiveCount.monitor() || $foo}</i> <!-- reset counts --> <button on:click={fooReflexiveCount.reset}>Reset</button>
-
Monitor state change counts (in code-snippets):
<script> const fooChangeCount = createReflectiveCounter(); $: fooChangeCount.monitor($foo); </script> <!-- reporter/resetter --> <i>$foo state change counts: {$fooChangeCount}</i> <button on:click={fooChangeCount.reset}>Reset</button>
You can see a live demo of ReflectiveCounters
in the Advanced Probes discussion.
Insight: Diagnostic probes are temporary
Diagnostic probes should be considered temporary, and removed (or commented out) when your analysis is complete.
There are techniques where they can be disabled at run-time (with minimal overhead), but this exercise is left up to the reader.
Demo App
Before we can begin any analysis, we will need some code to play with. It should be simple and focused, so we can concentrate on it's reactivity.
I have created an interactive demo (a Svelte REPL) that we can use.
The basic idea behind the demo is you can maintain the characteristics of a logged-in user (the top-half: EditUser.svelte
), and display them (the bottom half: GreetUser.svelte
) ... pretty simple :-) You can update one or more properties of the user by simply changing the text, and clicking the Apply Change
button. Go ahead and play with the interactive demo now!
The demo is broken up into a number of modules. I won't detail them here ... they are summarized in App.svelte
(of the Demo REPL).
SideBar: Normally the EditUser
/GreetUser
components would be mutually exclusive (i.e. displayed at different times) ... I merely combined them so we can better see the "reflexive correlation" between the two.
For our discussions, we will be focusing on a single module: the GreetUser
component.
GreetUser.svelte (see GU1_original.svelte
in Demo REPL)
<script>
import user from './user.js';
</script>
<hr/>
<p><b>Greet User <mark><i>(original)</i></mark></b></p>
<p>Hello {$user.name}!</p>
<p>
May we call you at:
<i class:long-distance={$user.phone.startsWith('1-')}>
{$user.phone}
</i>?
</p>
<style>
.long-distance {
background-color: pink;
}
</style>
This component merely greets the logged-in user (an object-based Svelte store), visualizing individual properties of the user. Long distance phone numbers will be highlighted (when they begin with "1-").
What could be simpler than this? This should provide a good basis for our discussions :-)
Inspecting App Reactivity
Let's enhance the GreetUser
component with our Diagnostic Probes (discussed in Exploring App Reactivity) to see how well it behaves.
Logging Probes
Here is our GreetUser
component with the Diagnostic Logging Probes applied:
GreetUser.svelte (see GU2_logDiag.svelte
in Demo REPL)
<script>
import user from './user.js';
// diagnostic probes monitoring reflection
const probe1 = () => console.log('Name section fired');
const probe2 = () => console.log('Phone class fired');
const probe3 = () => console.log('Phone section fired');
</script>
<hr/>
<p><b>Greet User <mark><i>(with reflexive diagnostic logs)</i></mark></b></p>
<p>Hello {probe1() || $user.name}!</p>
<p>
May we call you at:
<i class:long-distance={probe2() || $user.phone.startsWith('1-')}>
{probe3() || $user.phone}
</i>?
</p>
<style>
.long-distance {
background-color: pink;
}
</style>
You can run this version of the Demo REPL by selecting: with reflexive diagnostic logs.
Very nice ... by analyzing the logs, we can determine exactly when individual html-snippets are re-executed!
Advanced Probes
Let's also apply the Advanced Diagnostics (just for fun), to see what they look like:
GreetUser.svelte (see GU3_advancedDiag.svelte
in Demo REPL)
<script>
import user from './user.js';
import createReflectiveCounter from './createReflectiveCounter.js';
// diagnostic probes monitoring reflection
const probe1 = createReflectiveCounter('Name section fired');
const probe2 = createReflectiveCounter('Phone class fired');
const probe3 = createReflectiveCounter('Phone section fired');
</script>
<hr/>
<p><b>Greet User <mark><i>(with advanced on-screen diagnostics)</i></mark></b></p>
<p>
<mark>{$probe1}:</mark>
Hello {probe1.monitor() || $user.name}!</p>
<p>
<mark>{$probe2}/{$probe3}:</mark>
May we call you at:
<i class:long-distance={probe2.monitor() || $user.phone.startsWith('1-')}>
{probe3.monitor() || $user.phone}
</i>?
</p>
<style>
.long-distance {
background-color: pink;
}
</style>
You can run this version of the Demo REPL by selecting: with advanced on-screen diagnostics.
Great ... our component's reactivity is now visible, directly on our page!
Re-Render Analysis
So there seems to be some unexpected results, revealed through the introduction of our diagnostic probes. We are seeing html-snippets re-execute when their state did NOT change (ouch)!
You can see this by changing a single property (say name), and notice that all three of our html-snippets re-execute! You can even click Apply Change
button with no property changes, and still ... all three of our html-snippets re-execute! SideBar: I realize I can optimize the user
store to prevent this last scenario, but for the purposes of this discussion, it better highlights the point we are driving at.
So what is going on?
Dereferencing Objects
If you remember our discussion of Reactive Triggers, this is actually an example of an object reference being overly broad in it's dependency granularity.
<p>Hello {$user.name}!</p>
Because Svelte has marked the $user
object as being stale, any html-snippet that references that object will re-execute, regardless of whether the dereferenced .name
has changed or not!
At first glance, this seems counter intuitive. Why would Svelte do this? Is this in fact causing redundant and unnecessary re-renders in our DOM? ... Spoiler Alert: No redundant re-renders are occurring, but we will discuss this in the next section!
Well if you stop and think about this, in order for Svelte to monitor an object's dereferenced content, it would have to pre-execute sub-expressions found within the snippet, and monitor the resulting value.
In our simple example, that may technically be possible, but as a general rule this is a bad idea, for a variety of reasons.
The primary reason being that in order to accomplish this, these sub-expressions would always need to be executed, and that goes against the basic tenet of what Svelte is trying to do (i.e. it's reactive triggering mechanism) ... that is: Should this snippet be re-executed or not? If Svelte had to pre-execute parts of the snippet to make this determination, there could be negative side-effects! For example, the sub-expression could be invoking a method that applies unwanted mutations, etc.
SideBar: My explanation here is my "best guess", based on intuition. If I receive comments from "in the know" Svelte maintainers, I will make any needed corrections (to the explanation) and remove this SideBar :-) Regardless of the the explanation, this is in fact how Svelte works!
Svelte's Re-Render Optimization
So what does this mean?
The "elephant in the room" is: Is this actually producing redundant and unnecessary re-renders in our DOM? Remember: DOM updates are expensive! Is this really true, or is there more going on "under the covers"?
It occurred to me that just because Svelte decided to re-execute my html-snippet, doesn't necessarily mean that it resulted in a DOM update.
Could it be that Svelte further optimizes this process by insuring the result of an html-snippet actually changed? If you think about it, this makes a lot of sense for Svelte to do.
In this particular case, an unnecessary html-snippet re-executed because of an overly broad dependency granularity ... i.e. an object verses it's individual content (we discussed this in the Reactive Triggers section).
There are other cases, however, where our html-snippet could return the same result, even when it's dependencies legitimately change. Think about it: this is application code (outside the control of Svelte). Consider a case where our app requirements will group a set of enumerated values in to one classification, generating the same result from multiple values.
As it turns out, Svelte does in fact optimize it's DOM updates by insuring the content has actually changed ... so there are no redundant re-renders!
Svelte comes to our rescue once again!
I initially determined this by going into a debugging session of one of my diagnostic probes.
By stepping out one level (into the Svelte world), I found myself in some rather cryptic code, where a rather complex conditional was executing a helper function that actually performed the low-level DOM update.
Being a little uncertain about this complex conditional, I decided to merely set a break-point on that helper function.
This allowed me to interact with my app, and determine that: sure enough ... the DOM fragments only update (i.e. re-render) when the the result of the html-snippet actually changed!
THIS IS SOOO KOOOL!
Svelte Compiler Output
OK, now I was starting to get cocky. I started wondering: how efficient is Svelte in making this "content change" determination? I kept thinking more about this cryptic code where I found myself (in the debugging session):
Could it be that this code was the output of the Svelte compiler? ... You know, that "JS output" REPL tab, that you were always afraid to click on?
Sure enough my hunch was right!
With this newfound confidence, dare I attempt to make sense of this cryptic code? ... well it's worth a try!
Caveat: This section is completely optional. We have already discussed the key takeaway that you need to know on this topic. Therefore, this section is strictly for extra credit only (albeit very interesting to real geeks)! Feel free to skip ahead to the next section.
FYI: I won't clutter up the article with a lot of this cryptic code ... you can follow along by viewing the "JS output" tab from the Demo REPL.
So here goes ...
Cryptic Names:
The first thing you will notice is that the variable names in this code aren't incredibility intuitive ... mostly numbered variables with single letter prefixes. But hey: this is machine-generated code! We wouldn't want lengthy, intuitive names bloating the size of our bundle! Actually, once you get the hang of it, there are some useful patterns in the names ... keep reading.
DOM Fragments:
The most important takeaway of this code is that Svelte has managed to break down our html into fragments that can be re-built at the lowest level of our DOM tree.
This is a crucial point! Once this has been accomplished, it becomes rather trivial to incrementally process change!
My intuition tells me that this is probably the most complex aspect of the compiler.
-
For static html (that does not vary), it even uses a simple approach of
innerHTML
.For example, this:
<p><b>Greet User <mark><i>(original)</i></mark></b></p>
Generated this:
p0 = element("p"); p0.innerHTML = `<b>Greet User <mark><i>(original)</i></mark></b>`;
Now this, I can handle :-)
-
For dynamic html content (driven by an html-snippet/interpolation), it further breaks down the html into the needed individual DOM elements (which can be incrementally updated).
For example, this:
<p>Hello {$user.name}!</p>
Generated this:
// from the c() method ... p1 = element("p"); t4 = text("Hello "); t5 = text(t5_value); t6 = text("!"); // from the m() method ... insert(target, p1, anchor); append(p1, t4); append(p1, t5); append(p1, t6);
Notice that for dynamic content, Svelte is keeping track of two things:
- the
t5
text dom element - and the
t5_value
text content ... this must be the output of our html-snippet!
- the
Naming Conventions:
Are you starting to get a feel for some of the naming conventions?
-
p
is for paragraph -
t
is for text nodes - etc.
Component Methods:
The component contains several methods. In reviewing their implementation, I think I can infer the following characteristics:
// appears to be initializing our internal state
c() {
... snip snip
}
// appears to be the initial build-up of our DOM
m(target, anchor) {
... snip snip
}
// appears to be the incremental update of our DOM fragments
// ... THIS IS THE KEY FOCUS OF OUR REACTIVITY (analyzed below)
p(ctx, [dirty]) {
... snip snip
}
// appears to be removing our DOM
d(detaching) {
... snip snip
}
More on Naming Conventions:
Hey ... these names are starting to make sense, once you realize we are playing the Sesame Street Alphabet Game!
-
c()
is forconstructor()
-
m()
is formount()
-
p()
is forpartiallyPutinProgressivePermutations()
... I obviously have NO IDEA on this one :-( Mark later informed me it stands forupdate()
(using the second letter), and provided a reference to a Tan Li Hau resource ... where was this when I needed it? :-) -
d()
is fordestroy()
- There are a number of methods that are not operational (e.g.
i: noop
, etc.), so we obviously have NOT hit the more advanced cases with our very simple component :-)
Incremental Updates:
The primary method we are interested in is the p()
method. This is where the incremental DOM updates occur. It is where I found myself in the debugging session, when I determined that the DOM updates were optimized.
Notice it has 3 sections of code (each prefixed with a conditional -
if
)WowZee ... our component definition also has 3 html-snippets (what a coincidence)!
-
Let's look at one of them (I have reformatted the JS just a bit, and added the
//
comments):html code fragment
<p>Hello {$user.name}!</p>
compiled output
p(ctx, [dirty]) { // one of 3 sections ... if (dirty & /*$user*/ 1 && // conditional Part I t5_value !== (t5_value = /*$user*/ ctx[0].name + "")) { // conditional Part II set_data(t5, t5_value); // the payload - update the DOM! } ... snip snip },
Here is my analysis:
ctx[]
array contains all our dependencies.ctx[0]
happens to be our$user
object (thanks to the compiler retained comment hints).dirty
contains a bitwise accumulation of the "staleness" of ALL our dependent variables (one bit for each dependent).Part I of the conditional is pulling out the dirty flag of the
$user
dependent variable (using the bit-wise AND operator -&
). This determines if our$user
variable is stale. If it is, we will continue on to Part II (via thelogical-AND
operator -&&
).Part II of the conditional is actually doing two things: It is assigning the latest
t5_value
from our html-snippet (after converting it to a string:+ ""
), AND it is comparing the prior/next snippet output (using identity semantics:!==
). Only when the prior/next has changed will it execute the conditional payload (i.e. update the DOM). Ultimately this conditional is a very simple primitive string comparison!The
set_data()
function is a Svelte helper utility that actually updates the DOM! You can find these utils on GitHub here, or simply open them from your installednode_modules/svelte/internal/index.js
. This particular utility merely sets the supplied data in a DOM text element:
function set_data(text, data) {
data = '' + data;
if (text.data !== data)
text.data = data;
}
Svelte's Reflexivity is Very Efficient
Well that was fun! A very interesting exercise! What have we learned?
Don't be afraid to open the "JS output" tab of your REPL!
Big Bird would do well in a Svelte code review!
Most importantly, the following insight:
Insight: Svelte's reflexivity is very efficient!
The component's DOM representation is highly optimized, broken down into fragments that can be re-built at the lowest level of our DOM tree.
Dynamic content is gleaned from executing html-snippets.
Reflection is triggered by dependent state changes.
DOM updates occur only when the content has actually changed (using a very light-weight string comparison)!
Who could ask for anything more?
Kudos go out to Rich Harris and the Core Contributors for being so smart and thorough!
App Reactivity Tweaks
We have learned that there is a subtle distinction between reflection (Svelte's execution of html-snippets) and re-rendering (applying DOM updates).
Just because Svelte has decided to run an html-snippet (through it's dependency monitoring), doesn't mean that a DOM update is applied (although it typically is) ... because the snippet could return the same result. Svelte optimizes this process to insure DOM updates only occur when they actually change.
As a result, our reflection count can be slightly larger than the re-rendering count. There are two reasons for this:
An overly broad dependency granularity (e.g. the difference between objects and primitives). This one is on the shoulders of Svelte. As an example, Svelte has invoked our snippet because of an object change, but the object's sub-content (used by our snippet) hasn't really changed. We will discuss this further in: Finer Grained Dependency Management
The html-snippet could return the same result for multiple dependent values. This is on the shoulders of our App. Consider the case where our app requirements will group a set of enumerated values in to one classification, generating the same result from multiple values. We will discuss this further in: Preresolve Variations
Regardless of who's shoulders these conditions emerge, there are app-specific techniques by which we can narrow this gap (even to zero). So how can we impact this? After all, Svelte is the one who is in control of executing our html-snippets. How can we alter this?
The basic thrust of what we are about to do is to move a portion of our reflexivity FROM html-snippets TO code-snippets. Remember, we mentioned that code-snippets typically have less overhead (because they merely result in changes to JavaScript state variables).
Why would you want to do this? Does it really represent a significant optimization? Well, consider this:
- What if this discrepancy count were large (where we were needlessly re-executing an html-snippet many times with the same output)?
- And what if the overhead of executing this html-snippet were extremely high?
- What if the same html-snippet were needed in multiple places in our html?
Keep in mind, we do not have these conditions in our simple toy app ... but for sake of example, let's pretend that we do!
Regarding the optimization question ... to be honest, most likely the techniques we are about to discuss will not significantly impact your app's performance. In some cases, we will just be moving an optimization that Svelte was already doing, into the application realm. With that said, the best opportunity for an optimization is bullet point 3 (above).
So why go through this exercise? Very simply: to better understand the finer characteristics of Svelte's reactivity! This knowledge can give you the edge that separates the senior developers ... knowing the impact of finer-grained tweaks ... pulling in the big bucks ... we can only hope!
Extreme Optimization: Prefer reactivity in code-snippets verses html-snippets
The overhead of code-snippets are typically very light, because they merely change JavaScript state variables.
While it is true that variable changes can result in html DOM changes (by stimulating a dependent html-snippet), this will be canceled out, when the dependent variable is unchanged (within the code-snippet).
Finer Grained Dependency Management
This section addresses Svelte's overly broad dependency granularity, as it relates to Primitive Types verses Object Types.
Our GreetUser
component is currently dereferencing the $user
object within it's html. This is causing Svelte to execute our html-snippets in cases where the dereferenced property has not changed.
We can change this by simply normalizing our referenced state into primitive types.
Takeaway: Fine Tune Svelte's Dependency Management by using primitive types
When an html-snippet dereferences an object that has the potential of changing (e.g.
user.name
), this may result in a false-positive condition.Svelte will re-execute the snippet based on whether the top-level object has changed (e.g.
user
), and not the dereferenced result!You can change this by depending on primitive types normalized from the object (say through a code-snippet), giving the html-snippet finer granularity in what triggers it's execution.
html-snippets that depend on primitive types will only execute when their dependent values change (based on identity semantics).
This is really an application of the Extreme Optimization: Prefer reactivity in code-snippets verses html-snippets.
Here is our GreetUser
component with the applied change:
GreetUser.svelte (see GU4_primNorm.svelte
in Demo REPL)
<script>
import user from './user.js';
import createReflectiveCounter from './createReflectiveCounter.js';
// FOCUS: with primitive normalization
// normalize our referenced state with primitive types
// ... html-snippets will only fire when values actually change
// ... using JS identity semantics
$: ({name, phone} = $user);
// diagnostic probes monitoring reflection
const probe1 = createReflectiveCounter('Name section fired');
const probe2 = createReflectiveCounter('Phone class fired');
const probe3 = createReflectiveCounter('Phone section fired');
</script>
<hr/>
<p><b>Greet User <mark><i>(with primitive normalization)</i></mark></b></p>
<p>
<mark>{$probe1}:</mark>
Hello {probe1.monitor() || name}!</p>
<p>
<mark>{$probe2}/{$probe3}:</mark>
May we call you at:
<i class:long-distance={probe2.monitor() || phone.startsWith('1-')}>
{probe3.monitor() || phone}
</i>?
</p>
<style>
.long-distance {
background-color: pink;
}
</style>
You can run this version of the Demo REPL by selecting: with primitive normalization.
Great: Notice that the reflection counts (Svelte's execution of html-snippets) now correctly reflect actual changes to the corresponding state!
In this example, the "primitive normalization" was accomplished in the component code-snippet:
$: ({name, phone} = $user);
When the $user
object changes, this normalization code will be re-executed. However, because our html-snippets utilize the name
/phone
primitives, only the snippets that depend on the properties that truly changed will re-execute! ... very kool!
This "primitive normalization" can be accomplished in a variety of ways. In our example, it was carried out in the component code. Another way you could accomplish this is to promote derived stores, that pull a single value out. For example:
user.js (modified)
import {writable, derived} from 'svelte/store';
export const user = writable({
name: '',
phone: '',
});
export const name = derived(user, (u) => u.name);
export const phone = derived(user, (u) => u.phone);
Preresolve Variations
This section addresses the case where an html-snippet generates the same result for multiple dependent values. This typically occurs when the snippet contains conditional logic.
In our example, long distance phone numbers will be highlighted (when they begin with "1-"). This is accomplished by conditional logic in the html-snippet:
<i class:long-distance={phone.startsWith('1-')}>
... snip snip
</i>
The issue here is that Svelte will re-execute the html-snippet based on whether the dependent phone
changes, irrespective of whether the CSS class will change.
You can see this in the demo by changing the latter part of the phone number (keeping the prefix intact):
As you can see, this resulted in a higher number of reflection counts (Svelte's execution of html-snippets).
Solution:
If we were to move this logical condition into a code-snippet, the resulting html-snippet would result in fewer executions!
Takeaway: Fine Tune conditional logic by moving html-snippet variations into code-snippets
By allowing conditional expressions to be resolved in a code-snippet, the resulting html-snippet will fire less often.
This is really an application of the Extreme Optimization: Prefer reactivity in code-snippets verses html-snippets.
Here is our GreetUser
component with the applied change:
GreetUser.svelte (see GU5_variations.svelte
in Demo REPL)
<script>
import user from './user.js';
import createReflectiveCounter from './createReflectiveCounter.js';
// normalize our referenced state with primitive types
// ... html-snippets will only fire when values actually change
// ... using JS identity semantics
$: ({name, phone} = $user);
// FOCUS: with variations in code
// by allowing conditional expressions to be resolved in a code-snippet,
// the resulting html-snippet will fire less often.
$: classes = phone.startsWith('1-') ? 'long-distance' : '';
// diagnostic probes monitoring reflection
const probe1 = createReflectiveCounter('Name section fired');
const probe2 = createReflectiveCounter('Phone class fired');
const probe3 = createReflectiveCounter('Phone section fired');
</script>
<hr/>
<p><b>Greet User <mark><i>(with variations in code)</i></mark></b></p>
<p>
<mark>{$probe1}:</mark>
Hello {probe1.monitor() || name}!</p>
<p>
<mark>{$probe2}/{$probe3}:</mark>
May we call you at:
<i class="{probe2.monitor() || classes}">
{probe3.monitor() || phone}
</i>?
</p>
<style>
.long-distance {
background-color: pink;
}
</style>
You can run this version of the Demo REPL by selecting: with variations in code.
Great: Notice that the reflection counts (Svelte's execution of html-snippets) now correctly reflects whether the CSS class actually changed!
In this rendition, the variability is now accomplished in the component code-snippet:
$: classes = phone.startsWith('1-') ? 'long-distance' : '';
As a result, the html-snippet will only execute when the classes
variable actually changes.
Optimization Caveats
Here are a couple of "extras" to consider regarding optimization:
Insight: Optimization is only relevant when reactivity occurs for active components
The initial render of a component (i.e. when it is mounted) will unconditionally execute all it's html-snippets (regardless of dependencies, or conditionals).
If there is no reactivity after this, then there is no need to be concerned with these optimization techniques.
Keep in mind however, that reactivity can sometimes be difficult to predict. In addition, future revisions to your code may introduce reactivity. So be careful! It doesn't hurt to always follow these techniques.
Insight: Optimization is preferred but optional
If you neglect to follow these optimization techniques, your app will still work correctly. It just won't be as performant as it could be.
Extra Credit Exercise
For those who would like some extra credit, let me propose an enhancement to our ReflectiveCounter
(discussed in Advanced Diagnostics).
In it's current form, the ReflectiveCounter
is providing us a reflexive count (the html-snippet execution count).
Can you think of a way that it could provide both reflexive counts -and- re-render counts (that is ... of the DOM updates)?
This little exercise should separate the Geeks from the wannabes!
I won't give you the solution directly, but here is a very big hint ... The invocation will change:
FROM:
<i>{fooProbe.monitor() || $foo}</i>
TO:
<i>{fooProbe.monitor( () => $foo )}</i>
Are you up for the challenge? FYI: There is a hidden easter egg (tucked away somewhere) that reveals the solution! If you can't find it, just ping me in the comments below.
Who is this guy?
Just to give you a little of my background (as it relates to software engineering)...
I have been in the software industry for over 40 years. I'm probably the old guy in the room (retired since 2015). I like to say that I am a "current" developer from a different era, but gee whiz, it is getting harder and harder to stay current! Case in point: I'm just now learning Svelte, which has been out how long?
Needless to say, I cut my "programming teeth" 25 years before there was a usable internet (in the mid 70's).
I remember the great computing pioneer, Grace Hopper as a visiting lecturer, who at the age 73 imparted the computing insights of the day (which at it's core, wasn't all that different from today). She used great visual aids ... passing out nanoseconds, etc. Admiral Hopper was a senior way back then (in the mid 70's), so I suppose I shouldn't be too self conscious :-) Trivia point: she also coined the term: bug!
When I eventually started web-development (in the mid 90's), I was "all in" for this new Netscape technology called JavaScript! Even back then, we were providing reactivity at a page level, using this new innovation.
Over the years I have written a number of large-scaled SPAs (predating the SPA term), using pure JavaScript (i.e. there were no frameworks)! Believe me, providing large-scaled app-based reactivity is a daunting task, requiring some good underlying architecture, and ultimately a lot of code!
I actually skipped right over the jQuery phenomenon, and went straight into the new declarative frameworks ... first Angular, then React. This declarative approach never ceases to amaze me ... in realizing how much can be accomplished with so little code :-)
Svelte merely takes this progression to the next level! It provides all the benefits of a declarative approach, without the bloated in-memory run-time framework!
I have been contributing to open source since my retirement (in 2015). My most recent offering is a product called feature-u: a React utility that facilitates Feature-Driven Development.
I am a brand spanking new Svelter!
My first Svelte project (too early to publish) is a re-creation of my most prized project (in the early 90's). It was an "Engineering Analysis" tool, written in C++ under Unix/X-Windows. It had:
- schematic capture: with multiple functional decompositions of the master schematic
- executable control laws: through graphical flow diagrams that were executable
- simulation: driven by the control laws (animating one or more of the schematics and control laws)
- a symbolic debugger: also driven by the control laws
- auto generation of the embedded system code (derived from the executable control laws)
- Needless to say, this system has reactivity on steroids!
You can find me On The Web, LinkedIn, Twitter, and GitHub.
Summary
Well, this turned out to be a much "deeper dive" than what I had initially envisioned :-) We have covered a lot! I hope you enjoyed this little journey, and learned something as well!
A big thanks goes out to Rich Harris and the Core Contributors for making Svelte such an awesome product! I can't wait to see what the future holds in the next release!
Happy Computing,
</Kevin>
P.S. For your convenience, I have summarized my findings here. Each point contains a short synopsis, and is linked to the more comprehensive discussion.
-
Terminology: snippet, code-snippet, and html-snippet
Within this article, the term snippet refers to any JavaScript expression that Svelte reactively manages and invokes when it's dependent state changes.
Snippets may either be:
- code-snippets (referred to as Reactive Declarations and Reactive Statements)
- or html-snippets (commonly referred to as interpolation).
Ultimately, snippets provide the dynamics within our html markup (i.e. it's reactivity).
-
Insight: Reactivity is based on change in dependent state
Snippet execution is triggered when dependent state changes
primitive types trigger reactivity only when the value changes (based on identity semantics)
objects trigger reactivity per the entire object, not individual content
-
Takeaway: Monitor Svelte snippet invocations through logically-ORed prefixed expressions
You can detect when Svelte executes a snippet by prefixing it with a logically-ORed monitor.
This can be a simple logging probe or a more advanced ReflectiveCounter
-
Insight: Diagnostic probes are temporary
Diagnostic probes should be considered temporary, and removed (or commented out) when your analysis is complete.
There are techniques where they can be disabled at run-time (with minimal overhead), but this exercise is left up to the reader.
-
Insight: Svelte's reflexivity is very efficient!
The component's DOM representation is highly optimized, broken down into fragments that can be re-built at the lowest level of our DOM tree.
Dynamic content is gleaned from executing html-snippets.
Reflection is triggered by dependent state changes.
DOM updates occur only when the content has actually changed (using a very light-weight string comparison)!
-
Extreme Optimization: Prefer reactivity in code-snippets verses html-snippets
The overhead of code-snippets are typically very light, because they merely change JavaScript state variables.
While it is true that variable changes can result in html DOM changes (by stimulating a dependent html-snippet), this will be canceled out, when the dependent variable is unchanged (within the code-snippet).
-
Takeaway: Fine Tune Svelte's Dependency Management by using primitive types
When an html-snippet dereferences an object that has the potential of changing (e.g.
user.name
), this may result in a false-positive condition.Svelte will re-execute the snippet based on whether the top-level object has changed (e.g.
user
), and not the dereferenced result!You can change this by depending on primitive types normalized from the object (say through a code-snippet), giving the html-snippet finer granularity in what triggers it's execution.
html-snippets that depend on primitive types will only execute when their dependent values change (based on identity semantics).
This is really an application of the Extreme Optimization: Prefer reactivity in code-snippets verses html-snippets.
-
Takeaway: Fine Tune conditional logic by moving html-snippet variations into code-snippets
By allowing conditional expressions to be resolved in a code-snippet, the resulting html-snippet will fire less often.
This is really an application of the Extreme Optimization: Prefer reactivity in code-snippets verses html-snippets.
-
Insight: Optimization is only relevant when reactivity occurs for active components
The initial render of a component (i.e. when it is mounted) will unconditionally execute all it's html-snippets (regardless of dependencies, or conditionals).
If there is no reactivity after this, then there is no need to be concerned with these optimization techniques.
Keep in mind however, that reactivity can sometimes be difficult to predict. In addition, future revisions to your code may introduce reactivity. So be careful! It doesn't hurt to always follow these techniques.
-
Insight: Optimization is preferred but optional
If you neglect to follow these optimization techniques, your app will still work correctly. It just won't be as performant as it could be.
Top comments (5)
I have really enjoyed using Svelte for my projects for the last year or so and thought I knew why until now. I'm blown away by your analysis. I have navigated some of these tricky areas (e.g. difficulty getting reactivity when not using primitives and learning to reassign objects to trigger updates) without having really taken the time to understand what the Svelte is doing behind the scenes.
Thank you for the insights!
Nice and detailed article! Glad to see so much effort is put into it.
This is such a lengthy and well written post I'm gonna save it as a reference for later, I've been wanting to check Svelte for months now.
Excellent article, and a very courageous dive into the svelte's internals. Thanks for sharing your insight.
This is Gold :)