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.
When I first encountered Svelte, I thought to myself, "Hmm... is this another new front-end framework?" I didn’t pay much attention at that time. However, as I began to see more and more blog posts and websites utilizing it, my curiosity was piqued.
After checking out the official website's tutorials and documentation, I realized that Svelte approaches things differently from conventional "front-end frameworks." It operates at the syntax level, leveraging a compiler to produce concise and efficient code.
This statement caught my attention:
Svelte compiles your code to tiny, framework-less vanilla JS — your app starts fast and stays fast.
After trying it out, I found that, while I still had some thoughts on how to improve certain aspects coming from a React background, overall, I really enjoyed Svelte. It represents a concept that's fundamentally different from other frameworks like React and Vue.
I highly recommend everyone check out the talk by the Svelte author at YGLF — Rethinking Reactivity, to rethink what the essence of front-end frameworks truly is and what we can achieve. Below are my notes and reflections from that talk.
When We Say Reactivity, What Are We Talking About?
The essence of functional reactive programming is to specify the dynamic behavior of a value completely at the time of declaration — Heinrich Apfelmus
When using front-end frameworks, we often consider two things:
- Virtual DOM — ensuring performance during page rendering
- Reactivity — tracking changes in values
The core of front-end frameworks lies in data flow and tracking changes in data.
Reasons for Using Virtual DOM
The primary reason is that updating the entire UI when data changes can significantly impact performance. Therefore, Virtual DOM typically implements an efficient diff algorithm to ensure that only necessary changes are re-rendered during updates.
However, the challenge lies in developing a stable diff algorithm paired with an update mechanism, which requires substantial effort and careful performance consideration to avoid bottlenecks when the tree depth becomes too great.
Reactivity
To ensure React can track data updates (changes), React uses setState
and useState
to monitor value changes. Vue, on the other hand, employs a Proxy to allow developers to trigger reactive mechanisms when accessing values, thereby tracking data changes.
In React, we use useState
or this.setState
to ensure React can detect changes in values, along with a mechanism to prevent unnecessary updates (batch updates). Let's look at the code below:
const Counter = () => {
const [counter, setCounter] = useState(0);
const handleClick = () => {
setCounter(c => c + 1)
}
return <div onClick={handleClick}>{counter}</div>
}
Each time the component updates, useState
and the handleClick
function are re-evaluated. React internally keeps track of these state changes and performs the appropriate updates.
To address this issue, there are mechanisms like useMemo
and various optimization techniques:
shouldComponentUpdate
React.PureComponent
useMemo
useCallback
Implementing these mechanisms requires considerable effort just to avoid a complete re-render during updates and to provide developers with optimization options for Virtual DOM performance. This combination leads to a significantly large bundle size for React and react-dom.
The author even published a post on the official blog titled Virtual DOM is pure overhead to explain the trade-offs brought by Virtual DOM. Here’s an excerpt from the end of the article:
It's important to understand that virtual DOM isn't a feature. It's a means to an end, the end being declarative, state-driven UI development. Virtual DOM is valuable because it allows you to build apps without thinking about state transitions, with performance that is generally good enough. That means less buggy code, and more time spent on creative tasks instead of tedious ones.
But it turns out that we can achieve a similar programming model without using virtual DOM — and that's where Svelte comes in.
Additionally, there was a Twitter post in the article that presented an interesting perspective:
Why is the conclusion that there's something wrong with the framework, instead of something being wrong with the platform? If the DOM provided a way to efficiently supply large trees created functionally, without React having to do a diff, we’d use that API.
— Vim Diesel ⚛️🆁 (@jordwalke) July 8, 2018
The entire Virtual DOM mechanism exists because frameworks want to alleviate the burden of state management, but why isn’t such a mechanism provided by the platform itself? I find this viewpoint quite intriguing, but I'm uncertain how much effort it would take to implement, and if browsers were to do it, we might face cross-browser issues and standards compliance — the pace of evolution could lag behind that of the frameworks themselves.
While it may not seem that massive, when combined with the code of the product itself, this bundle size is quite surprising.
To summarize the thoughts above, the drawbacks of modern front-end frameworks include:
- Runtime reactivity + Virtual DOM diff mechanism
- Bulky bundle size
Some might wonder: Why should we care so much about bundle size and performance?
Rethink Performance
I used to think this way too. With CPUs like the i5 and i7 being commonplace and smartphones like the iPhone 11 Pro being standard, is it really necessary to be concerned about these issues?
Recently, I’ve developed new thoughts based on my experiences living and working in Japan:
- Not everywhere is like Taiwan, where you can get unlimited data plans at a low price; in Japan, there typically aren’t unlimited options, and data is both limited and expensive. This is a LINE Mobile plan, and while general social media usage doesn’t count towards data, as a YouTube and Netflix enthusiast, it often isn’t enough.
- Many users opt for budget plans from less expensive providers, where base stations and speeds are not as fast or stable.
- Internet speed noticeably slows down in the subway, unlike the more stable speeds in Taiwan.
Taken together, if you open a webpage while on the subway or using a budget plan with poor service, you can truly feel the speed differences. I’ve also come to realize that not every country enjoys the same fortunate internet speeds as Taiwan.
Moreover, when I used a Raspberry Pi, I could also feel the significant lag when lower-tier CPUs were rendering web pages.
In this era of rising IoT, not everyone will use high-end CPUs and GPUs to run web applications. This has gradually led me to contemplate performance and framework choices. Does Svelte encounter this issue? Let’s continue to explore.
At around 19:15 in the talk, the author used Dan's example of React time-slicing and implemented it in Svelte. He discovered that Svelte doesn't require enabling Async mode or debouncing; its performance significantly outpaces React.
Embracing Native APIs
While going through the documentation, I noticed their event propagation mechanism is essentially just addEventListener
and CustomEvent
; there’s no Synthetic Event (like in React) or event pooling — it’s just CustomEvent.
// https://github.com/sveltejs/svelte/blob/master/src/runtime/internal/dom.ts#L60-L63
export function listen(node: Node, event: string, handler: EventListenerOrEventListenerObject, options?: boolean | AddEventListenerOptions | EventListenerOptions) {
node.addEventListener(event, handler, options);
return () => node.removeEventListener(event, handler, options);
}
// https://github.com/sveltejs/svelte/blob/master/src/runtime/internal/dom.ts#L275-L279
export function custom_event<T=any>(type: string, detail?: T) {
const e: CustomEvent<T> = document.createEvent('CustomEvent');
e.initCustomEvent(type, false, false, detail);
return e;
}
However, this might lead to bugs caused by browser differences in the future, or optimizations that could be done within the framework might need to be handled by developers themselves, like Event Delegation or React Event Pool. Whether this is a plus or minus depends on the context of use.
Irresistible Syntax
I love using syntax and language-level solutions to problems; these approaches allow developers to save time and avoid delving into the inner workings of useEffect
, useMemo
, and useCallback
.
Svelte offers a simple syntax (mostly still JavaScript) and template syntax, which, after compilation, transforms into lower-level JavaScript code. For example:
<script>
import { createEventDispatcher } from 'svelte';
export let label;
let a = 1;
$: b = a * 2;
const dispatch = createEventDispatcher();
function handleClick() {
dispatch('toggle', {
value
});
}
</script>
<button class="button" class:active on:click={handleClick}>{label}</button>
The compiled output of this Svelte code looks like this:
import { createEventDispatcher } from "svelte";
function create_fragment(ctx) {
let button;
let t;
let dispose;
return {
c() {
button = element("button");
t = text(/*label*/ ctx[0]);
attr(button, "class", "button");
},
m(target, anchor, remount) {
insert(target, button, anchor);
append(button, t);
if (remount) dispose();
dispose = listen(button, "click", /*handleClick*/ ctx[1]);
},
p(ctx, [dirty]) {
if (dirty & /*label*/ 1) set_data(t, /*label*/ ctx[0]);
},
i: noop,
o: noop,
d(detaching) {
if (detaching) detach(button);
dispose();
}
};
}
let a = 1;
function instance($$self, $$props, $$invalidate) {
let { label } = $$props;
const dispatch = createEventDispatcher();
function handleClick() {
dispatch("toggle", { value });
}
$$self.$set = $$props => {
if ("label" in $$props) $$invalidate(0, label = $$props.label);
};
let b;
$: b = a * 2;
return [label, handleClick];
}
class App extends SvelteComponent {...}
Notice that the element
and text
functions are merely wrappers for document.createElement
and element.textContent
, with no concept of Virtual DOM involved.
So how does Svelte achieve its reactivity mechanism?
What Can the Compiler Do?
When you declare let a = ''
, the compiler knows a
has been declared. Thus, when you perform an operation like a = a + 1
, the compiler will transform it to something like $$invalidate('a', a + 1)
, notifying Svelte that this value has changed. At this point, Svelte marks this variable as dirty and schedules an update for any parts of the UI that use a
.
If a
is not actually used in the UI (within a tag), Svelte may not even compile it out, for example:
<script>
let name = 'world';
function handleClick() {
name += '?';
}
</script>
<h1 on:click={handleClick}>Hello </h1>
function create_fragment(ctx) {
//
}
function instance($$self) {
let name = "world";
function handleClick() {
name += "?"; // << Note this line; since it's not used in the tag, it doesn't use $$invalidate
}
return [handleClick];
}
// ...
If it is used in a tag:
<script>
let name = 'world';
function handleClick() {
name += '?';
}
</script>
// Original "world" replaced with variable name
<h1 on:click={handleClick}>Hello {name}</h1>
function create_fragment(ctx) {
// ...
}
function instance($$self, $$props, $$invalidate) {
let name = "world";
function handleClick() {
$$invalidate(0, name += "?"); // << Uses $$invalidate instead
}
return [name, handleClick];
}
class App extends SvelteComponent {
constructor(options) {
super();
init(this, options, instance, create_fragment, safe_not_equal, {});
}
}
export default App;
This way, unnecessary updates are avoided, showcasing one of the benefits of introducing a compile phase. During the talk, it was mentioned:
This article highlighted that through the power of compilers, we can effectively reduce the runtime code executed and liberate ourselves from the constraints of the language, allowing the compiler to take on more responsibilities similar to static languages.
The template syntax is also well thought out; for example:
<ul>
{#each Object.values(list) as item}
<li>{item.name}</li>
{/each}
{#if condition}
<p>true!</p>
{/if}
<button on:click={handleClick}>click me</button>
</ul>
For more details, please refer to the official documentation. Here, I want to emphasize that this syntax is very straightforward, allowing any experienced developer to learn the entire set of syntax in a short period. Additionally, because it’s compiled, it can also perform syntax and static checks.
A Taste of Svelte
Recently, I created a webpage that organizes fish data from Animal Crossing for easy lookup. Its features include:
- Search (purely front-end)
- Table sorting
- Rendering tables based on configuration
That's all there is to it.
(The search behavior is a bit odd; I’ll fix it when I have time XD)
This project would take some time to write in pure JavaScript, and using React would have felt like overkill, so I decided to use Svelte. The result was very satisfying; the entire JavaScript bundle is only 8.2KB (after gzipping), and on a 3G network, it loads in just 160ms.
Is it Magic?
In the talk on Rethinking Reactivity at 17:11, the author also touched on this perspective, where Evan (the creator of Vue) mentioned that this isn’t really JavaScript; it's SvelteScript.
That would make it technically SvelteScript, right?
— Evan You (@youyuxi) October 30, 2018
In essence, frameworks are somewhat like magic (abstracting complex details). React transforms complex update mechanisms into magic, Vue packages template syntax into magic, and everyone is performing their own version of magic. However, React and Vue work their magic at runtime, while Svelte delegates this work to the compiler.
Comprehensive Store and Context Mechanism
Svelte has built-in store capabilities that can be utilized, and its simple usage looks like this. Since it's built-in, you don't have to rely on various redux-xxx
solutions.
Interaction
Svelte also offers numerous surprises in terms of syntax and implementation, such as built-in transitions along with spring, fly, and flip mechanisms, accompanied by corresponding directives for developers. When wanting to add interactions (micro-animations), you don’t need to worry about what library to use; just reference it directly from Svelte, and it looks like this:
// copied from https://svelte.dev/examples#tweened
<script>
import { tweened } from 'svelte/motion';
import { cubicOut } from 'svelte/easing';
const progress = tweened(0, {
duration: 400,
easing: cubicOut
});
</script>
<style>
progress {
display: block;
width: 100%;
}
</style>
<progress value={$progress}></progress>
The syntax is quite intuitive, allowing developers to manipulate the DOM more effectively.
Another common issue when it comes to animations is interruption; if a user interacts during an animation, without special handling, the transition usually waits until the end before restarting, which can significantly affect the user experience.
Svelte's transitions are inherently interruptible. For detailed information, you can check the transition chapter. For instance, clicking a checkbox could trigger fade-in and fade-out animations that continue from the current progress, regardless of how quickly you click.
It’s worth noting that you can directly use {#if visible}
for transitions, unlike React, where you need to maintain component rendering to preserve animations.
Note that the transition is reversible — if you toggle the checkbox while the transition is ongoing, it transitions from the current point, rather than the beginning or the end.
If you were to rewrite this in React, you might need libraries like react-transition-group or react-spring to facilitate such functionality. In contrast, Svelte integrates it directly into the framework, eliminating the need for additional packages, and even combining it with syntax for a more intuitive experience.
Conclusion
After discussing all these drawbacks, I don’t intend to criticize any framework or elevate Svelte to an unrealistic pedestal. React has undoubtedly rewritten the history of front-end and web development, and I genuinely appreciate the concept of React hooks. However, the concepts and results offered by Svelte are too captivating, prompting me to delve deeper into its workings.
Furthermore, regarding concerns about bundle size and performance, I believe that one day hardware and network speeds will catch up, at which point the differences will become less pronounced. Techniques such as code-splitting and dynamic imports can also effectively reduce initial bundle sizes.
With powerful tools like Babel, we can leverage JavaScript along with Babel's support to minimize the complexities during runtime, and advancements in low-level APIs like WebAssembly and ArrayBuffer may usher in a new era in web development. In this day and age, it seems that beyond mastering front-end development, one might also need to learn how to create simple compilers.
If you found this article helpful, please consider buying me a coffee ☕ It'll make my ordinary day shine ✨
☕Buy me a coffee