Experiences and Caveats of Svelte 5 Migration

Experiences and Caveats of Svelte 5 Migration


I have recently updated a rather complex web application. The application has features like auth, Stripe, i18n, dark/light mode, PWA, etc. Overall, it has around 30 pages and components, with almost no third-party npm packages.

I would like to point out what I found quite challenging when migrating the app to Svelte 5.



Auto-Migration Script Hammer

The auto-migration script provided by Svelte can do the job for you with this “one-liner” command in the terminal npx sv migrate svelte-5 (after you do all the necessary updates and installs: “@sveltejs/vite-plugin-svelte”: “^4.0.0” and “svelte”: “^5”). But I do not recommend this “hammer” approach.

Go file by file, component by component with Ctrl + Shift + P (Windows/Linux) / Shift + Command + P (Mac) and use the Migrate Component to Svelte 5 Syntax command in the VS Code command palette instead. You will have more control that way.



Deprecated run() Surprise

The script cannot perform miracles. Upgrading reactive variable declarations to $state() is usually fine. However, the script may struggle to detect whether $: should be converted to $derived()/$derived.by(() => {}) or $effect(() => {}).

So, guess what? With the auto-migration script, you might end up with lots of run(() => {}).

For example, I used this pattern in my app:

<script>
...
   let notext = false;
   $: if (data.completeDoc == 'NoLangVersion') {
      notext = true;
   }
   $: if (data.completeDoc !== 'NoLangVersion') {
      notext = false;
   }
</script>

...
{#if notext}
   {data.userPrefferedLang.noTextWarning}
{:else}
...
{/if}
...
Enter fullscreen mode

Exit fullscreen mode

The auto-migration script will give you this:

<script>
    import { run } from 'svelte/legacy';
...
    let notext = $state(false);
    run(() => {
        if (data.completeDoc == 'NoLangVersion') {
            notext = true;
        }
    });
    run(() => {
        if (data.completeDoc !== 'NoLangVersion') {
            notext = false;
        }
    });
</script>
Enter fullscreen mode

Exit fullscreen mode

with a nice little warning that the run function is deprecated.

The better Svelte 5 code would be this I guess:

<script>
...
    let notext = $derived.by(() => {
        if (data.completeDoc == 'NoLangVersion') {
            return  true;
        }
        if (data.completeDoc !== 'NoLangVersion') {
            return false;
        }
    });
...
</script>
Enter fullscreen mode

Exit fullscreen mode

The reason is that the script cannot transform code to $derived.by(() => {}) easily, so it would like to use a more dirty approach with $effect(). But $effect() runs only client-side, so the script uses the deprecated run function instead.



Avoid $effect If You Can

Now we are getting to the most important takeaway. Which is $effect() running only client-side. So no $effect() on the server, for prerendering pages and SSR.

$effect() DOES NOT RUN ON THE SERVER!

This should be really emphasized in the Svelte 5 documentation.

Look at this two examples:

<script>
let a = 1
let b = 2

$: c = a + b
</script>

{c}  // server responds with c == 3
Enter fullscreen mode

Exit fullscreen mode

<script>
let a = $state(1)
let b = $state(2)
let c = $state(0)

$effect(() => {
  c = a + b
})
</script>

{c}  // server responds with c == 0
Enter fullscreen mode

Exit fullscreen mode

They are not the same. This causes a lot of challenges. The client will need to reevaluate the c variable when mounting the page. The page will look different when sent from the server and when finally DOM-rendered on the client (SSR, SEO, flicker issues, etc.).

So always try to use $derived or $derived.by(() => {}) over $effect(). It will save you lots of trouble.

It’s quite the same story as when we were discouraged from using stores in SvelteKit and SSR.



$effect vs onMount() in SvelteKit

You might be tempted to replace your onMount() in SvelteKit with $effect() thanks to the examples that were given during the arrival of Svelte 5. For the reasons already mentioned, I would discourage this for the time being. onMount is still a a core Svelte lifecycle hook.



$bindable $props Surprise

The other nice surprise is that Svelte 5 takes care to have consistent variable values. If you pass a variable as a prop to a component and change this variable in the component later on, the script will try to solve this inconsistency using $bindable $prop. The parent should be notified, so your app state is consistent.

Look at this example:

// parent svelte file
<script>
   import ComponentBinded from './ComponentBinded.svelte';
   import ComponentWithDerived from './ComponentWithDerived.svelte';
   let name = $state('John Wick');
</script>

<p>Name value in parent: {name}</p>

<ComponentBinded bind:name={name} />

<ComponentWithDerived {name} />
Enter fullscreen mode

Exit fullscreen mode

The autou-migration script will want you to use a component with binded value to ensure the parent may get the updated value back:

// ComponentBinded.svelte
<script>
   let { name = $bindable() } = $props();
   name = name.toUpperCase()
</script>

<p>
Name value in component with binded value: {name}
</p>
Enter fullscreen mode

Exit fullscreen mode

But maybe we can use quite simpler way as well, you guessed it, with $derived():

// ComponentWithDerived.svelte
<script>
   let { name } = $props();
   let upperCaseName = $derived(name.toUpperCase())
</script>

<p>
Name value in component with derived value: {upperCaseName}
</p>
Enter fullscreen mode

Exit fullscreen mode



:global { } Block

A very nice feature that I found during migration was that we can use CSS :global with block now. Styling with :global is quite necessary if you want to style the HTML elements in @html, for example.

So instead of this:

...
<style>
    #blog :global(table) {
        width: 100%;
    }
    #blog :global(td) {
        text-align: left;
    }
    #blog :global(th) {
        font-weight: bolder;
        font-size: medium;
        text-align: center;
    }
</style>
Enter fullscreen mode

Exit fullscreen mode

you can use this:

...
<style>
   #blog :global {
    table {
        width: 100%;
    }
    td {
        text-align: left;
    }
    th {
        font-weight: bolder;
        font-size: medium;
        text-align: center;
    }
}
</style>
Enter fullscreen mode

Exit fullscreen mode



Style as a Prop in Components

In Svelte 4, if you wanted to provide a CSS class as a prop to a component, you would use {$$props.class}:

// Icons Component
<script>
   export let name;
   export let width = '1.5em';
   export let height = '1.5em';
   export let focusable = false;

   let icons = {
    user: {
    svg: `<path fill="none" d="M0 0h24v24H0V0z"/><path d="M12 6c1.1 0 2 .9 2 2s-.9 2-2 2-2-.9-2-2 .9-2 2-2m0 10c2.7 0 5.8 1.29 6 2H6c.23-.72 3.31-2 6-2m0-12C9.79 4 8 5.79 8 8s1.79 4 4 4 4-1.79 4-4-1.79-4-4-4zm0 10c-2.67 0-8 1.34-8 4v2h16v-2c0-2.66-5.33-4-8-4z"/>`
    },
    user_logged: {
    svg: `<path fill="none" d="M0 0h24v24H0z"/><path d="M12 12c2.21 0 4-1.79 4-4s-1.79-4-4-4-4 1.79-4 4 1.79 4 4 4zm0 2c-2.67 0-8 1.34-8 4v2h16v-2c0-2.66-5.33-4-8-4z"/>`
    }
   };

   let displayIcon = icons[name];
</script>

<svg
    xmlns="http://www.w3.org/2000/svg"
    class={$$props.class}
    viewBox="0 0 24 24"
    fill="currentColor"
    {focusable}
    {width}
    {height}
>
   {@html displayIcon.svg}
</svg>

<style>
    ...
</style>

Enter fullscreen mode

Exit fullscreen mode

In Svelte 5 you may use class={className}:

<script>
   let {
    name,
    width = '1.5em',
    height = '1.5em',
    focusable = false,
    class: className = ''
    } = $props();

let icons = {
    user: {
    svg: `<path fill="none" d="M0 0h24v24H0V0z"/><path d="M12 6c1.1 0 2 .9 2 2s-.9 2-2 2-2-.9-2-2 .9-2 2-2m0 10c2.7 0 5.8 1.29 6 2H6c.23-.72 3.31-2 6-2m0-12C9.79 4 8 5.79 8 8s1.79 4 4 4 4-1.79 4-4-1.79-4-4-4zm0 10c-2.67 0-8 1.34-8 4v2h16v-2c0-2.66-5.33-4-8-4z"/>`
    },
    user_logged: {
    svg: `<path fill="none" d="M0 0h24v24H0z"/><path d="M12 12c2.21 0 4-1.79 4-4s-1.79-4-4-4-4 1.79-4 4 1.79 4 4 4zm0 2c-2.67 0-8 1.34-8 4v2h16v-2c0-2.66-5.33-4-8-4z"/>`
    }
   };

   let displayIcon = icons[name];
</script>

<svg
    xmlns="http://www.w3.org/2000/svg"
    viewBox="0 0 24 24"
    fill="currentColor"
    class={className}
    {focusable}
    {width}
    {height}
>
    {@html displayIcon.svg}
</svg>

<style>
...
</style>

Enter fullscreen mode

Exit fullscreen mode



Possible Lighthouse Perfomance Drop

When I used the auto-merging script, I was shocked at how my app’s performance dropped. With Svelte 4, I had nearly all 100%s. It was only after I manually migrated and carefully considered how (mainly how to avoid $effect() if possible) that my Lighthouse scores were back in the green again.



Final Words

It took longer to migrate to Svelte 5 than I had expected. I still have not pushed this new version to production, though. The updates to Svelte 5 are still coming in with quite high frequency.

I hope my experience may be useful to others.



Source link
lol

By stp2y

Leave a Reply

Your email address will not be published. Required fields are marked *

No widgets found. Go to Widget page and add the widget in Offcanvas Sidebar Widget Area.