Svelte 5 is Ready for Production

Svelte is already quite popular thanks in part to its simplicity and great dev experience. With version 5, the framework gets even better due to a more efficient compiler, a new highly optimized reactivity system, and a handful of other quality-of-life improvements.

What’s interesting though is the overall focus on simplicity. What you might not realize is that the entire frontend space is rapidly changing, and everyone is racing towards lighter, more efficient solutions. Svelte is at the forefront of this race, and in this article, we’ll review 4 of the new features that might help it become the winner of the most desired title in frontend development.

1. Highly Optimized Compiler

Most modern frontend frameworks are heavily relying on a compiler. Thanks to this intermediate step, you can build your apps using a simple intuitive API, and your code

<script>
  let count = 0;
</script>

<button onclick="{()" ="">count +=1}> {count}</button>

is rewritten under the hood into the hot pile of garbage needed to keep your UI updated and responsive in the browser.

// Svelte v4
import "svelte/internal/disclose-version";

function create_fragment(ctx) {
    let button;
    let t;
    let mounted;
    let dispose;

    return {
        c() {
            button = element("button");
            t = text(/*count*/ ctx[0]);
        },
        m(target, anchor) {
            insert(target, button, anchor);
            append(button, t);
            if (!mounted) {
                dispose = listen(button, "click", /*click.handler*/ ctx[1]);
                mounted true;
            }
        },
        i: noop,
        o: noop,
        d(detaching) {
            if (detaching) {
                detach (button);
            }
            mounted = false;
            dispose();
        }
    };
}

function instance($$self, $$props, $$invalidate) {
    let count = 0;
    const click_handler = () => $$invalidate(0, count += 1);
    return [count, click handler];
}

class App extends SvelteComponent {
    constructor(options) {
        super();
        init(this, options, instance, create fragment, safe_not_equal);
    }
}

This is the process Svelte reimagined in v5. The compiler is now more tightly engineered leading to code that is smaller, more robust, and, of course, faster.

So while in the past compiled code might have looked like a mess for the uninitiated, Svelte 5 outputs JavaScript that even I can understand once Chat GPT explains it to me a few times.

// Svelte v5

import * as $ from "svelte/internal/client";

var on_click = (_, count) => $.set(count, $.get(count) + 1);
var root = $.template(`<button> </button>`);

export default function App($$anchor) {
  let count = $.mutable_source(0);
  var button = root();

  button.__click = [on_click, count];

  var text = $.child(button);

  $.template_effect(() => $.set_text(text, $.get(count)));
  $.append($$anchor, button);
}

$.delegate(["click"]);

2. Runes

Those familiar with the framework probably know that building apps in Svelte feel like building apps in plain old HTML and vanilla JavaScript. All complexity is conveniently hidden under the hood, and the entire experience feels like magic.

Despite the apparent simplicity, before version 5, the reactivity mechanism used in Svelte components

<script>
    let count = 1;
    $: double = count * 2;
</script>

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

was different from the one used in Svelte stores.

import { writable } from "svelte/store";

export const userProfile = writable({
  isLoggedIn: false,
  role: null,
});

This distinction was both confusing and frustrating since you had to do quite a bit of refactoring if you had to move code around.

Thanks to runes, which are functions powered by signals, reactivity is unified and behaves the same both in Svelte and TypeScript files.

The new dev experience is clean and intuitive, and I have to highlight that Svelte is keeping some of its magic since you can modify arrays or objects directly, and the framework will detect the changes and update the DOM accordingly.

<script>
    let numbers = $state([]);
    const count = $derived (numbers.length);

    $effect(() => {
        console.log(count);
    });

    function add() {
        numbers.push (Math.floor(Math.random() * 10));
    }

</script>

<button onclick={add}>Add</button>

<ul>
    {#each numbers as number}
        <li>{number}</li>
    {/each}
</ul>

Compare this with the majority of other frameworks, which rely on updating the object reference to identify the change.

const [numbers, setNumbers] = createSignal<number[]>([]);

function add() {
    const number = Math.floor(Math.random() * 10);
    setNumbers([...numbers(), number]);
}

3. Component Composition

Smart code reusability makes wonders when it comes to a maintainable code base. Svelte 5 introduces snippets and render tags as a way to create reusable chunks of markup inside your components.

Let’s look at this example, where we have a few duplicated lines in our conditional rendering process.

{#each images as image}
    {#if image:href}
        <a href={image.href}>
            <figure>
                <img src={image.src} />
                <figcaption>{image.caption}</figcaption>
            </figure>
        </a>

    {:else}
        <figure>
            <img src={image.src} />
            <figcaption>{image.caption}</figcaption>
        </figure>
    {/if}
{/each}

A possible option to solve this in the past was to create an Image component, but usually, this brings on overhead I don’t want to waste time with.

With snippets, you can now define reusable code blocks directly in the same component and also enjoy features like parameter destructuring or passing snippets to other components.

{#snippet figure (image)}
    <figure>
        <img src={image.src} />
        <figcaption>{image.caption}</figcaption>
    </figure>
{/snippet}

{#each images as image}
    {#if image.href}
        <a href={image.href}>
            {@render figure(image)}
        </a>
    {:else}
        {@render figure(image)}
    {/if}
{/each}

4. Better Event Handling

In Svelte 4 event listeners were defined using the on-colon syntax.

<script>
    import { createEventDispatcher } from 'svelte';

    const dispatch = createEventDispatcher();
</script>

<button on:click={() => dispatch('colder')}>
  colder
</button>

<button on:click={() => dispatch('warmer')}>
  warmer
</button>

Listeners are plain attributes now, which makes things way simpler when handling component events.

<script>
    let {colder, warmer} = $props();
</script>

<button onclick={colder}>
  colder
</button>

<button onclick={warmer}>
  warmer
</button>

This might not seem like a big update, and, is just Svelte playing catch up with what’s considered a standard these days.

All these features are the building blocks for a framework that, honestly, offers one of the easiest ways to build modern, reliable web apps, so, if you didn’t do it already, now is the time to seriously look into Svelte.

If you feel like you learned something, you should watch some of my youtube videos or subscribe to the newsletter.

Until next time, thank you for reading!