Svelte: The Compiled Future of Front End

When I learned React in 2014 it was a revelation. Coming from Angular 1, React was a breath of fresh air in a world full of foot-guns, confusing best practices, and unclear jargon (services, models, directives, oh my!) Instead, the future was simple: "everything is a component". Components have props, components have state, and that's about it. This singular concept eliminated entire categories of problems front end developers faced.

The front end community clearly agreed. React skyrocketed in popularity becoming the most popular choice for building front ends. Once other libraries started adopting component model, it was clear people saw React was on to something.

Components everywhere

Other frameworks began following React's lead. The team behind Angular incrementally introduced components, then rewrote the framework from the ground up to be more component-centric. New frameworks like Vue also emerged following a component model. With all the big hitters adopting components, it seemed like the front-end community had "figured out" a way to make web apps that scale.

But the React team knew there was more work to be done. class components for example didn't fit well with React's declarative model. So the team continued to evolve React with novel features like hooks. And soon (as of writing this), the team will introduce the long-awaited concurrent rendering.

These new features are awesome! I have nothing but praise for the hard work the React team has done to put out these features. They solve hard problems developers face every day in their React apps. I'm also impressed with how the team has listened to the community and iterated accordingly. It's clear they want to provide a great experience for developers and users.

However.

The more I read about concurrent mode, and the more I use hooks, the stronger I feel something's off. The implementations of these features feel like low-level primitives that should be abstracted away from application code. They should be handled implicitly by the library and I shouldn't have to think about it.

Let's look at an example handling side-effects with hooks:

function useMyHook({ arg1, arg2 }) {
  useEffect(() => {
    doSomeSideEffect(arg1, arg2)
  }, [arg1, arg2] /* <-- this feels wrong! */)
}

function MyComponent() {
  useMyHook({ arg1, arg2 }) /* <-- must be called unconditionally! */
  
  return (...)
}

Every time I write a dependency array, or smack myself for forgetting the rules of hooks I find myself asking, why am I doing this by hand? I'm lazy. Can't the framework figure this out for me?

I get why these rules exist. Dependency arrays rely on reference equality to determine when to update. And that fits in with React's existing model that assumes you're using immutable data structures.

But surely there must be a better way?

I found one solution when I discovered a newer contender in the ever-evolving framework landscape: svelte.

Svelte has entered the chat

Svelte is not entirely new. Its initial release was back in 2016. Like many, I acknowledged svelte had some neat ideas but I largely ignored it. Ultimately, I gave it a resounding "huh" and moved on.

It wasn't until recently when I took some time to get to know svelte that I found it's doing something I've not seen from other UI frameworks.

First, let's look at  a comparison:

// react
// App.js
function App() {
    const [count, setCount] = React.useState(0)

    useEffect(() => {
        doSomethingWithCount(count);
    }, [count])
    
    return (
        <button onClick={() => setCount(count + 1)>
            count: {count}
       </button>
    )
}

// svelte
// App.svelte
<script>
  let count = 0;
    
  $: doSomethingWithCount(count);
</script>

<button on:click={() => count += 1}>count: {count}</button>

Both these examples implement a button that increments a count state by one when clicked. They then execute a side effect called doSomethingWithCount whenever count changes.

For React developers, the React implementation probably looks pretty familiar.

But if you're not familiar with svelte, the $: syntax and direct re-assignment (count += 1) might look a little weird.

Setting aside the fact that $: is actually valid javascript, svelte is really hijacking these operations for its own purpose. It does so using the secret sauce that makes svelte so compelling: the compiler.

A compiler changes everything

Svelte is a compiled framework (or transpiled if you want to get pedantic): you write your components in .svelte files, run those through the svelte compiler, and get back out generated javascript that manages state changes, running side-effects, and rendering your app.

Svelte isn't unique in having a compile step. Most modern front end frameworks these days like React and Vue document the happy path that includes a compilation step.

These libraries do include instructions for skipping the compile step. But it's a niche option most developers skip over. (At least one survey supports this with 92% of apps being powered by webpack)

Svelte however is the first framework I've come across that wholeheartedly embraces the compiler. Svelte treats the compiler as a fundamental building block that enables simple, but powerful concepts like we saw above.

Let's look at one more aspect of svelte that showcases the compiler:

// store.js
import { writable } from 'svelte/store';

export const count = writable(0);

// App.svelte
<script>
  import { count } from './store';
  
  $: doSomethingWithCount($count);
</script>

<button on:click={() => $count += 1}>count: {$count}</button>

This has the similar behavior as our earlier count example. But now, count is a svelte store for managing state. It's a singleton that can be shared across components similar to a React context, or Vuex store. The power of the compiler comes into play when it comes to the $ as we see in $count.

count is a normal object with set, update, and subscribe functions (useful for consuming stores in plain .js files). But, when you prepend $ to your count store in a .svelte file, svelte finds this at compile time and transforms it into an observable value. {$count} is now a number that will update any time the store changes.

Svelte's API focuses on developer productivity. The developer defines their intent as a simple statement (eg, $count += 1), and svelte does the heavy lifting at compile time to make your updates happen as efficiently as possible.

This efficiency carries forward to DOM manipulation too. Since svelte can use its compiler to safely make assumptions on how your app works, it's able to make updates to the DOM without a virtual DOM.

So long vDOM

The virtual DOM (vDOM) is how libraries like React and Vue detect something has changed and the DOM needs to be updated. However, the vDOM creates overhead. These frameworks spend precious render time computing a diff of what's changed in the vDOM. For a lot of cases this is fine and users won't ever notice. But the overhead adds up and can result in slower renders for large updates. When you get into that situation it's not always clear where to start fixing it.

React has plans to solve this with it's upcoming concurrent rendering APIs. However, svelte eliminates this overhead entirely with direct DOM mutation.

What?! But mutation is a sin! I hear you say. Fear not as this the work of svelte's compiler in action. Similar to React, the mutation happens behind the scenes. What's different about svelte is that it uses the compiler to determine developer intent and know exactly what needs to change. No vDOM diffing overhead required.

But isn't react compiled too?

I mentioned earlier that React and Vue also have a (recommended) compile step. While svelte embraces the compiler, similar libraries downplay its existence.

For example, in React you could skip the compile step and everything left is proclaimed to be "just javascript". This is a powerful concept in its own right, but what this tells you is React isn't doing as much with the compile step as it could. And that is part of why we are left with the awkwardness of hooks.

But then you have JSX – the compiler-required syntax for React – which is all but synonymous with React. You will rarely find an example in the wild that doesn't include it. It's clear the community has adopted JSX as a standard. If JSX is standard, then so is a compile step.

This muddies the waters on what React is communicating. On one hand you have "the compile step is just syntactic sugar and optional; everything is 'just javascript'". On the other hand it's "you should really use the compile step unless you know what you're doing".

Compare this to svelte which has embraced the compiler and thereby freed itself from the limitations of javascript. Svelte has designed its own syntax that's tailored for simply solving complex front end problems.

So if React has a compilation step, what's stopping it from also embracing compilation as a means to improve developer and user experience?

Well, there's evidence to say it could happen. At one point the react team was exploring the concept of a "compiled" react using a project called prepack. While the project is shelved for the time being, there's a chance they will pick it back up.

Also, React is known for continuously reinventing itself over the years. React originally released with createClass, then introduced class components, and most recently added hooks. Additional compiler features would fit in as just another iterative evolution of React.

I'm confident React isn't done iterating. It's hard to say if React will embrace compilation like svelte has. But React team has a solid track record of continuously pushing the boundaries of what React can do. React would be well-served to draw inspiration from svelte and embraces something like svelte's compiler.

Is svelte better than react?

From all the great things I've said about svelte, and the shortcomings I've raised about react, you might think my answer is "yes". My answer is actually "no". Or rather, "it's complicated".

Svelte is great. It has a ton of features I don't have time to cover that are a joy to use and cleanly solve complicated front end problems. Some examples include accessibility helpers, form management helpers, and even a full blown animation library.

But, React has at least one aspect going for it that outclasses svelte right now: community.

The number of great options for building front ends continues to grow. Having a strong community has proven to be an important factor to the success of a library. Up-to-date learning materials, active forums for asking questions, and other libraries that supplement the core are just a few aspects that contribute to a healthy ecosystem.

Users of a library thrive when a healthy ecosystem enables them to be productive. And a library thrives when its users thrive.

Community management is a full-time job. Backed by Facebook, React has a full-time team dedicated to the success of react. Svelte on the other hand lacks such support of a major tech corporation.

Even though React has some awkwardness, the awkwardness is mitigated by a legion of helpful experts, and an abundance training materials and third-party libraries.

The future of svelte

The svelte community is small, but growing. Svelte has an uphill battle to reach the size of community that React has gathered.

Maybe a large community is not svelte's goal right now. If it isn't, that's fine! In fact, it may be in svelte's best interest to continue its steady pace of growth. Svelte can continue to gather feedback from its dedicated community and hone its ideas. Then, when the library is ready, so will the community.

At the end of the day, both these libraries are wonderful. And this isn't a a winner-take-all-scenario. There is room for both libraries to thrive side-by-side.

What I believe is most likely to happen is that these libraries will continue to draw inspiration from each other and other similar libraries. They will continue iterating on their solutions and ultimately developers will benefit.  

Who knows, maybe one day my laziness will win out and I won't need to write dependency arrays by hand.