Svelte — очень хороший фреймворк/библиотека, но она имеет чувствительный недостаток (где их нет…) — это система отслеживания изменений, она плохо ловит мутации данных, поэтому в Svelte комьюнити форсируется иммутабельный подход.

И у меня давно чесались руки попробовать систему отслеживания похожую на ту что используется в Angular для Svelte, и вот появились свободные выходные и я на скорую руку из «говна и палок» состряпал Svelte-подобный компилятор (Svelte-M), что дало положительный побочный эффект в плане размера бандла и скорости работы:

Размер бандла получился почти в 2 раза меньше (todo приложение):

Svelte: 4.7k (2.2k gzipped)

Svelte-M: 2.7k (1.2k gzipped)

Скорость работы:

Рендеринг 5000 элементов: Svelte 894ms, Svelte-M 563ms (Svelte-M быстрее: 63% от времени Svelte).

Удаление 1 элемента: Svelte 113ms, Svelte-M 38ms (Svelte-M быстрее в 3 раза).

Пере-рендеринг (удаление и добавление 5000 элементов): Svelte: 859ms, Svelte-M 418ms (в 2 раза быстрее).


Пример todo-приложения на Svelte-M: example.html и оно же собраное в бандл на jsfiddle.

Предупреждение: Svelte-M — это просто 2х дневный эксперимент, там мало функционала и куча ограничений, не нужно рассматривать его как конкурента или использовать в продакшене 😉

За счет чего скорость? В Svelte DOM собирается по элементам, + отдельно атрибуты и пр. что снижает производительность и раздувает код, а в Svelte-M шаблон вставляется одной операцией, и элементы списков клонируются целиком через Node.cloneNode (тоже одной командой, вместо поэлементной сборки), и меньше оберток в результирующем коде.

Теперь самое главное — это система отслеживания (я бы её назвал Bind-Checking), покажу отличия, где оно помогает:

  let todos = [];

  const add = () => {
      todos.push({name: 'Hello!'});
  };

Когда вы вызовите ф-ю add(), Svelte не отловит что todos изменился и DOM не обновится. Чтобы оно заработало в Svelte вам нужно куда-нибудь вставить присвоение (“=”) в эту функцию, самое простое присвоение это todos=todos — после этого пример начинает работать (это мне напоминает ручное обновление через setValue как это было раньше). Но Svelte форсурет использовать иммутабилити, поэтому в Svelte комьюнити мне подкинули такой вариант:

todos = todos.concat([{name: 'Hello!'}]);

А в Svelte-M нормально работает и первый вариант (как и любой другой).

Ок, едем дальше — у меня выводится список этих задач и мне кликом по задаче нужно что-то в ней изменить, например поправить текст задачи:

<script>
    let todos = [];
    const fix = (todo) => {
        todo.name += '!';
    }
</script>

<ul>
    {#each todos as todo }
    <li>
        <span on:click={() => fix(todo)}>{todo.name}</span>
    </li>
    {/each}
</ul>

Тут при клике вызывается ф-я «fix» и я меняю текст задачи — но оно не работает (изменений нет на экране), нужно вручную дернуть триггер — дописать todos=todos, хотя в комьюнити мне предложили более кошерный имутабельный вариант:

$: {
    fix = todo => {
        todos = todos.map(i => {
            return i === todo ? {
                name: i.name + '!'
            } : i;
        })
    }
}

Конечно же я им не воспользовался ;), кроме того имутабельность тут не причем, тригер срабатывает потому что тут появилось присвоение todos = ..., в Svelte-M, опять же, работает и первый вариант.

В итоге вариант с Bind-Checking делает приложение ещё ближе к javascript, требуется меньше трюков, и работает достаточно быстро. Таким бы я хотел видеть Svelte…

Если кто хочет попробовать исходники тут: github.com/lega911/svelte-m

Кроме того там есть одно синтаксическое отличие в биндингах: вместо

<span on:click={() => fix(todo)}>

Надо писать так:

<span on:click={fix(todo)}>