Svelte and the future of web development

Web development frameworks are a double-edged sword. On the one hand, frameworks allow you to accomplish more for less — to be more productive, write more maintainable code, for less effort, less overrall complexity (no matter how you define it), and so on. Like craft tools, the newer and shinier ones always seem to solve a broader range of problems than their predecessors, or at least promise to do so.

On the other hand, there's always the feeling, when using a framework, that you're moving too far away from the bare-metal of the browser, that many of the problems that the framework seems to abstract away have the side-effect of shielding you from understanding their true origins in the browser, and the result is a more pernicious form of vendor lock-in, where your ability to solve the major problems of the web without the help of your primary "tool" diminshes to uncomfortable levels.

It's the reason why in recent years it's become natural to classify oneself as an "Angular Developer", "React Developer", "Vue Developer", and so on. To me, this is the real problem of modern web development, and while the issues at stake are simple to understand, the trade-offs can be huge, and your decisions when building web applications will in all likelihood depend much more on factors separate from the framework itself. It becomes both art and science, and goes by the name of architecture.

Svelte

Recently I’ve been reading about a new front-end framework called Svelte. Its approach to rendering web applications is nothing short of a paradigm shift, in that it compiles your application to plain JavaScript, instead of enhancing it by taking advantage of features provided by the framework's code itself. This means that the framework (Svelte) is not present in the final output bundle of your app, even though the output bundle will be shaped by it.

In order to truly appreciate the significance of this approach, we have to look to how some of today's popular frameworks build web applications. Let's take React, Vue, and Angular as examples. Both React and Vue internally use something called the Virtual DOM (VDOM), which is a JavaScript Tree-like representation of the browser DOM. When you write React or Vue code, those libraries generate a VDOM of your whole application (or certain subsections of it), and use it to intelligently update only the parts that need updating on the real DOM.

This was done in order to minimize the process of browser reflow — or the recalculation of DOM element positions on the page and their CSS styles, since these operations were considered expensive for the browser to continuously run, especially on devices with low processing power. If React or Vue recognizes that there are no differences between the current VDOM and the previous VDOM, they simply don't apply any updates to the browser (real DOM).

Why this kind of optimization was needed, in turn, can only be understood by looking at what kinds of tools developers were using to update the DOM before any of these frameworks emerged on the scene.

The Good Olden Days

Before the rise of popular front-end frameworks, JQuery was considered the web development framework of the day. JQuery provided web developers with a simple API to directly manipulate the DOM and update the elements on the page based on new state or backend API data, like so:

$("#mytitle").text("Hello World");
$('.item').removeClass('hidden')
$('.item').addClass('shown');

However, a problem occurred when your web app relied heavily on fresh API data from the backend to render its UI. Think of a trading application with multiple graphs, tables, and screens side by side, loading new data by the second, or, better yet, think of Facebook in its early days where you had your friend status panel, notifications panel, news feed panel, and several other parts of the UI updating simultaneously based on new API data.

When you made an AJAX call to fetch this new data, you generally had no information about whether the new data will be similar to existing data or different. Of course, the expectation is that it might be different, and that's why you made the API call, but it could just as well that nothing changed. In both cases, what typically happened is that you re-rendered your whole application (or more reasonably, certain sub-sections of it), leaving to JQuery the hard work of going out there and updating the DOM, like so:

 // feeling old yet?
 $.get("/api/data", function(newData){
    renderApp(newData)
  });

function renderApp(data) {

    // new title data
    var titleElement = $('.titleElement')
    titleElement.text = data.title

    // update UI with new table data
    $('#tableWrapper').removeChild('table');
    var newTable = createTable()
    for (var i = 0; i < data.list.length; i++) {
        const newRow = createRow(data.list[i])
        newTable.append(newRow)
    }
    $('#tableWrapper').append(newTable)

}

I'm exaggerating in this example by calling the rendering method renderApp, but generally speaking, large portions of the UI were needed to update based on this new data, and it required a lot of work to remove/create new nodes consistent with this new data. Since you did not know just how this data would change, you wiped everything out and created new nodes for that data.

React's solution to this problem can be thought of as a way of automating the work necessary to minimize unneeded updates to the DOM. That solution is called the "Virtual DOM" (VDOM), and around it, an API that allows you to declaratively specify how your UI should look like based on that data. Since the Virtual DOM would be a JavaScript object, it was easier to generate it if there was a way to build your whole application using JavaScript. To that end, JSX was created, which is essentially a superset of JavaScript that also allows you to write HTML-like code, combining both in one language.

Vue also uses a VDOM, but instead of JSX, it allows you declare components as a combination of separate JavaScript, CSS, and HTML pieces. It uses an HTML-based template syntax that extends valid HTML code to allow for JavaScript expressions and string interpolations.

What about Angular? Well, Angular does not, strictly speaking, actually use a VDOM. Instead, it implements its own change-detection mechanism through the help of zones. After updating any state in an Angular component that is referenced in your HTML, Angular runs through all the components of your application in an effort track which values have changed, and if it finds any, updates the appropriate HTML elements on the DOM. This approach is conceptually similar to how the Virtual DOM is used in both Vue and React, but its implementation is different.

The Problem With Runtime Constructs

Interestingly, the one thing common to all 3 frameworks is the fact that they do the bulk of their work during runtime. The generation of the VDOM and the reconciliation process in React & Vue, as well the as Angular's change-detection cycle, all take place whenever state changes in your application. The impact of this runtime is not trivial — and both the React & Angular docs mention the performance issues your application may face if you don't play by the framework's rules.

Optimizing your code in React often takes the form of overriding the lifecycle methods (in particular, shouldComponentUpdate) to determine if your component should re-render or not (or using React.memo() when using hooks). Similarly, in Angular, having to update your change-detection strategy to onPush in order to prevent large component subtrees from needlessly dirty-checking.

Optimizing your application's performance when using these frameworks can be frustrating, and I've often wondered whether that work can be dealt with more effectively by the frameworks themselves than by the developer.

The final bottleneck, when all your performance issues have been addressed to the best of your ability, will be the runtime of the framework itself, and in large web applications, this (again) will not be trivial. Unfortunately, you cannot optimize it away without leaving behind the framework.

Build-time magic

What we'd really like to know is this: is it possible to know, at build time, the way the state of your application can change over time, and map out the relationship between this state and your UI, without invoking any preformance-hogging runtime constructs (VDOM or Component Tree) ?

As a matter of fact, the answer is yes! This is exactly what Svelte does. At its core, Svelte takes in your application's code, and transforms it into a pure JavaScript equivalent that allows your UI to react to the state changes in your application.

How it does this is part genius, and part magic, and I find the genius part easier to explain.

All state changes in JavaScript applications involve assignments. For example, if I do:

let myVar = 5; 
myVar = 10

I create a simple myVar variable, and update its value (state) to a new value using the assignment operator. After I assign it a new value, the challenge is how I can notify or signal to my UI that this particular value has changed, in order that it can properly reflect this new state. You also want to make sure that, if this new state is identical to the previous one, your UI should not update.

We already have this information at build time: any UI piece that is consuming this state, is the one that should be triggered to update. Svelte does exactly that. In the final output bundle, it follows the above assignment with a number of method calls to trigger the UI dependants of this state, like so:

let myVar = 5; 
myVar = 10
$$invalidate('myVar', (myVar = 10))

This $$invalidate marks the variable dirty and schedules an update for the component.

Conceptually, all this is doing is making sure to update only the areas of your UI that depend on that state. And if the values are different, then when your application is running, that update would be a normal DOM update, something along the lines of:

document.getElementById("myElement").textContent = "Updated Content"

Svelte's core job (performed at build time) lies in correctly mapping the state of your application to the specific areas of your UI that depend on that data. Because of the nature of this process, the word framework does not really describe what Svelte does; since it takes your Svelte code and produces a version that is most optimized for the browser to run (while still using JavaScript), it behaves more like a compiler targeting an environment rather than a framework supporting an application.

Seen this way, React and Angular's approaches seem like a roundabout way of reaching the same goal. The original creator of Svelte, Rich Harris, seems to agree with this, and I don't think I've heard a proper rebuttal explaining why constructs like the VDOM and the Component Tree are still necessary after performing the same kind of build-time optimizations that Svelte does.

Svelte's Build Pipeline

Svelte's build pipeline can divided into 3 main stages: preprocess, parse, and compile.

  • preprocess: This is the stage where Svelte takes in your Svelte code, and pre-processes it for subsequent stages of the compilation process. This usually takes the form of either transpiling your Svelte TypeScript code into JavaScript (TypeScript -> JavaScript) or transforming SASS to CSS (SCSS -> CSS), but it could be any form of processing, as long the output of this stage is valid Svelte code.
  • parse: After pre-processing ends, parsing begins. In the parsing stage, Svelte takes each of the three different types of code of your Svelte components - HTML, CSS, and JS - and parses them to produce an Abstract Syntax Tree (AST) of their inter-relationships. An AST is a Tree-like representation of the hierarchical relationships of the particular code that’s being parsed. In the case JS, Svelte uses an already well-known JavaScript parser library called acorn. In the case of CSS, it uses the library css-tree. Svelte produces the AST for HTML by itself.
  • compile: The final & most important stage of the process is the compile stage. This stage can be taken to be the whole argument for Svelte, or why it exists. In this stage, Svelte takes in the different types of ASTs generated in the previous stage, and iterates over them to produce the final JavaScript output bundle of your code. It also allows generating code for SSR, and in that case will produce a string literal corresponding to the HTML pages of each of the separate routes of your application.

Svelteland

At the heart of Svelte is the concept of a "Component", and a Svelte app is built by composing a number of components together. Here's what a simple Svelte component looks like (helloWorld.svelte):

<script>
 const helloWorld = 'Hello World'
</script>

<h1>{helloWorld}</h1>

<style>
 h1 {
   margin: 0 auto;
 }
</style>

In a way, this is very similar to how you'd define a Vue component — as a combination of separate CSS, HTML, and JavaScript parts. Like Vue, Svelte allows you to declare props, hook into lifecycle methods, and create computed values for your component, in addition to a lot of other features that are unique to Svelte. It is beyond the scope of this article to explain all of these features in detail, but you can find all the different ways you can use Svelte components in the documentation.

Is Svelte for you?

Svelte's main "market differentiator" is performance. On JS Framework Benchmarks it beats React, Vue and Angular on every single benchmark. On top of that, the same app written in Svelte will produce a much smaller output bundle than either React, Vue, or Angular. Those two points make Svelte a great candidate for apps that need to effeciently run on memory-constrained and low-powered devices, such as mobile devices (not so much today), and embedded devices of all types.

On the other hand, Svelte's development experience is not very different from using Vue, and is not compelling enough on its own to attract more developers into using it. In addition, being a newer framework, it lacks the kind of community support that other frameworks can boast to have. For most many developers, the ability to stackoverflow any question you might possibly face when using a framework is indispensable, and it's doubtful whether Svelte will ever compare in that regard to the popular frameworks mentioned above.

Frameworks and the future of web development

Can there be too many frameworks? A multitude of frameworks, while rich in variety and offers to satisfy the delicate tastes of every sort of developer, can only be confusing to someone whose goal is not frameworks, but to develop a product. A developer targeting the .NET ecosystem isn't expected to learn and be familiar with 6 different flavours of C#, so why should it be different when targeting the browser?

Most web developers unconsciously recognize this fact, and quickly build up their skills for a specific stack that they deem useful to that end. As a result, React is quickly becoming the lingua franca of modern web development, in much the same way as English is to cross-national communications. And just like English, it will gain this status not because it is inherently superior as a tool than all the rest, but only because of the influence of React-using developers across the world.

But it's possible that the future of web development will be different. Perhaps a more unifying standard will emerge that will allow developers to use the time-tested principles of software engineering directly to solve the problems of the web. With the advent of WebAssembly, you could write your whole app using Rust, C++ or C# (Blazor), with better performance gains than using JavaScript. Even though the consensus is that WebAssembly will never replace JavaScript entirely on the Web, it is possible that one of those languages will emerge as the standard for building web apps in the future, and I'm excited to see what those teams come up with as the browser becomes the primary environment for developing any and all applications.