Первый взгляд на Solid.js

Привет, на связи Antihype JS. Сегодня рассмотрим набирающий популярность frontend фреймворк - Solid. Сравним его с React, рассмотрим плюсы и минусы.

Первый взгляд на Solid.js

Solid имеет похожее на React API. Он также использует JSX для наших компонентов и построен на концепции однонаправленного потока данных.

Однако, Solid не использует абстракции по типу VDOM для детекта изменений и внесения их в DOM. Фреймворк построен вокруг собственной системы реактивности (сигналы) и компилятора. За счет этого, Solid может работать напрямую с DOM, показывать значительно лучшую производительность и низкое потребление памяти.

Пример простого компонента на Solid:

import { createSignal } from 'solid-js' export const App = () => { const [count, setCount] = createSignal(0) const increment = () => setCount(count() + 1) return ( <div> <p>Antihype JS</p> <button onClick={increment}> {count()} </button> </div> ) }

Ничего не напоминает?

Реактивность

Реактивность в Solid построена на специальных примитивах, так называемых сигналах. Если вы когда-либо сталкивались с MobX, то этот подход будет вам знаком. Для лучшего понимания работы сигналов рекомендуем изучить статью от автора библиотеки.

Сигнал и функция для изменения его значения создаются с помощью createSignal:

import { createSignal } from 'solid-js' const [count, setCount] = createSignal(0)

Обратите внимание, что count является не значением, а функцией.

При этом, создавать сигналы можно в любом месте вашей программы. В отличие от хуков в React, которые работают только в контексте компонентов.

Для отслеживания значения сигнала и создания сайд-эффектов используется createEffect:

import { createEffect } from 'solid-js' createEffect(() => console.log("count:", count()))

Эффект автоматически подписывается на изменения используемых внутри сигналов. Больше никаких массивов зависимостей.

Производный сигнал создается с помощью createMemo:

import { createSignal, createMemo } from 'solid-js' const [firstName] = createSignal('') const [lastName] = createSignal('') const userName = createMemo(() => `${firstName()} ${lastName()}`)

Сигналы, созданные с помощью memo, также автоматически отслеживают свои зависимости. Производные сигналы кэшируются и обновляются при изменении значения любого из сигналов зависимостей.

Жизненный цикл компонента

import { createSignal, onMount, onCleanup } from 'solid-js' export const Counter = () => { const [count, setCount] = createSignal(0) const increment = () => setCount(count() + 1) onMount(() => console.log('mount')) onCleanup(() => console.log('unmount')) return <button onClick={increment}>{count()}</button> }

В Solid наши компоненты вызываются только один раз, а затем всю работу по обновлению DOM делает система реактивности.

Для подписки на событие монтирования компонента в DOM используется функция onMount, для анмаунта - функция onCleanup.

Больше никаких ререндеров компонентов.

Компилятор JSX

Solid использует свой компилятор JSX шаблонов для работы напрямую с реальным DOM.

После компиляции шаблоны превращаются в оптимизированный JS код для точечного обновления элементов.

Например, наш компонент:

import { createSignal } from 'solid-js' export const App = () => { const [count, setCount] = createSignal(0) const increment = () => setCount(count() + 1) return ( <div> <p>Antihype JS</p> <button onClick={increment}>{count()}</button> </div> ) }

после компиляции превращается в следующий JS код:

const _tmpl$ = /*#__PURE__*/template(`<div><p>Antihype JS</p><button>`); const App = () => { const [count, setCount] = createSignal(0); const increment = () => setCount(count() + 1); return (() => { const _el$ = _tmpl$(), _el$2 = _el$.firstChild, _el$3 = _el$2.nextSibling; _el$3.$$click = increment; insert(_el$3, count); return _el$; })(); };

Функция создает DOM элементы, привязывает обработчики событий, делает подписку на обновления нужных узлов.

Подписка создается внутри функции insert:

Если запустить наш проект в браузере, то мы увидим, что при изменении значения счетчика обновляется только наша кнопка - parent:

Первый взгляд на Solid.js

Чтобы ваш код максимально эффективно работал с DOM, библиотека предлагает следующий набор встроенных компонентов для работы с условиями и списками:

<Show />

<Show when={joinedAntihypeJS()} fallback={<button onClick={join}>Join Antihype JS</button>} > <p>Welcome</p> </Show>

when - сигнал предикат

fallback - работает как ветка else, контент будет показан, если сигнал joinedAntihypeJS вернет значение false

<For />

<For each={subscribers()}> {(s, i) => ( <p> {i() + 1}: {s.name} </p> )} </For>

При изменении массива subscribers <For> обновляет или перемещает узлы в DOM, а не создает их заново. Для работы необходимо передать только сигнал массив в пропс each.

Вместо передачи компонента в children <For />, мы передаем колбэк. Первый аргумент - элемент массива, второй - индекс.

Индекс является сигналом по умолчанию. Это сделано для оптимизации при изменении порядка элементов в массиве. Элементы DOM будут поменяны местами и не будут удаляться и создаваться заново.

Bundle size

Воспользуемся сервисом bundlejs для сравнения размера библиотек Solid и React.

react + react-dom: 138kb в минифицированном виде и 44kb в gzip

solid-js: 19kb в минифицированном виде и 7kb в gzip

Первый взгляд на Solid.js
Первый взгляд на Solid.js

Performance

Постоянная ссылка на это сравнение на сайте: js-framework-benchmark. Выбрали нативные Solid и React, а также их комбинации с различными стейтменеджерами.

Время рендеринга при манипуляциях со строками таблицы:

Первый взгляд на Solid.js

Метрики, снятые при старте приложения:

Первый взгляд на Solid.js

Потребление памяти:

Первый взгляд на Solid.js

Как видно, производительность Solid в этом бенчмарке сравнима с реализацией на нативном JS.

Экосистема

К сожалению, на текущий момент Solid не может похвастаться такой богатой экосистемой, как React. На сайте библиотеки собраны официальные пакеты и пакеты от сообщества. Уже существуют биндинги для разных стейтменеджеров, роутеры и порты дизайн систем.

Заключение

Использовать Solid в в продакшен проекте или нет - решать исключительно вам. Но наша редакция советует не проходить мимо этого инструмента.

Если вам понравился контент и вы интересуетесь фронтенд-разработкой, то подписывайтесь на наш телеграм-канал.

1818
19 комментариев

1) чем createSignal в solid отличается от useState в react?
2) как вы планируете пробрасывать эти сторы между компонентами?
3) зачем поддувать комментарии пустыми аккаунтами?

3
Ответить

1 - useState это хук
Хуки можно вызвать только внутри компонента, вне мира реакта хуки не работают
При изменении стейта в реакте перерендеривается все дерево ниже, чтобы этого избежать вам надо обмазаться React.memo
В солиде же такой проблемы нет изначально, каждые узлы дома зависят от своих сигналов
2 - props, context, стейтменеджеры (createStore из solid/effector/reatom/storeon/различные tanstack-solid-query/solidapollo)
3 - это наши друзья, а не пустые акки

4
Ответить

Но зачем, если есть божественный Vue?

4
Ответить

не исключено!

Ответить

На удивление выглядит не так уж и плохо. Можно даже попробовать, но вряд ли уйдет в продакшн, все таки бизнес любит более устоявшиеся решения. Ну а в эпоху LTE и 5G за размер конечного bundle можно уже не париться))))
Я когда увидел сколько весит приложение банков я порядком так сказать о*уел. Российский Сбер или ВТБ - 530 мегабайт, Карл! Некоторые из этих подопытных умудряются кэша насобирать на 170-800 мегабайт. Боже что вы там кэшируете в таким объемах? Это новый тикток? У меня в каждом тока по одной платежной карте, что там может столько весить?!

3
Ответить

По этой же причине держу на телефоне минимум приложений, больше веб версиями пользуюсь.
Про размер бандла и 5G соглашусь :)

1
Ответить

Вот фигня с кжшем и правда бесит... Чего там такого может храниться...

1
Ответить