Первые впечатления от SolidJS

  • воскресенье, 6 ноября 2022 г. в 01:18:00

В последнее время начал рассматривать замену ReactJS’y, так как уже немного поднадоело работать с одним и тем же.

Это не значит, что я собираюсь в ближайшее время отказываться от ReactJS и уйти в сварщики.

Я просто считаю, что пользоваться одной технологией всю свою карьеру по причине "ну, она же решает мои задачи", не очень правильным.

Разработчик всегда должен расширять свой кругозор знаний и рассматривать альтернативные технологии, даже если они сегодня тебе и не пригодятся.

И перед дальнейшим прочтением этой заметки просто держите в голове, что это лишь моё субъективное мнение. Через полгода я могу думать о SolidJS совершенно другое, как и о ReactJS.

Говоря про SolidJS

Итак, раскрою сразу все карты и скажу честно, что на сегодняшний день я не считаю SolidJS лучшей заменой ReactJS’у.

Да, в некоторые местах React значительно уступает Solid’у, особенно в производительности и в размере сборки приложения, но в остальном, это тот же самый React, только со своими особенностями.

React-компонент

_10
function App() {
_10
return <h1>Hello!</h1>
_10
}

Solid-компонент

_10
function App() {
_10
return <h1>Hello!</h1>
_10
}

И нет, это не опечатка. Все тот же самый JSX и те же функциональные компоненты из ReactJS.

Вот пример уже немного сложней. Реализация счётчика на ReactJS и SolidJS.

ReactJS
SolidJS

_22
import { useState, useEffect } from 'react';
_22
_22
function App() {
_22
const [count, setCount] = useState(0);
_22
_22
useEffect(() => {
_22
console.log('Счётчик изменился на ' + count);
_22
}, [count]);
_22
_22
useEffect(() => {
_22
console.log('Компонент монтирован');
_22
return () => console.log('Компонент размонтирован');
_22
}, []);
_22
_22
return (
_22
<div>
_22
<h1>{count}</h1>
_22
<button onClick={() => setCount(count + 1)}>+</button>
_22
<button onClick={() => setCount(count - 1)}>-</button>
_22
</div>
_22
);
_22
}

Как ты можешь заметить, в SolidJS есть три хука:

  • createEffect => что-то похожее на useEffect, только зависимости определяет автоматически (их не нужно указывать вторым аргументом в массив). Также, эта функция не умеет определять монтирование и размонтирование, как это делает useEffect. Для этого есть отдельные хуки.
  • createSignal => то же самое, что и useState
  • onMount => хук выполнится при первом рендере компонента.
  • onCleanup => хук выполнится при удалении компонента со страницы.

createEffect

Нравится ✅

Для меня createEffect выглядит намного удобней и понятней, чем useEffect, который выполняет роль швеца, жнеца и на дуде игреца.

Программисту меньше приходится думать о зависимостях, из-за которых будет тригериться его хук, так как компилятор SolidJS этот вопрос решает автоматически.

Не нравится ❌

Это, конечно, круто.

Но что если я не хочу следить за всеми зависимостями, что использую в createEffect?


_10
const [count1, setCount] = createSignal(0);
_10
const [count2, setCount] = createSignal(0);
_10
_10
// Что если мне не нужно следить за count1 внутри эффекта?
_10
createEffect(() => {
_10
console.log(count1 + count2);
_10
});
_10
_10
return <h1>{count1}</h1>

Конечно же на это есть решение, которое, так себе:


_10
createEffect(
_10
on(count2, (value) => {
_10
console.log(value, count1());
_10
})
_10
);

Таким образом, с помощью функции on(...) эффект будет следить за изменением только count2.

Уж лучше бы вторым аргумент добавили массив зависимостей 🤷🏻‍♂️

createSignal

Нравится ✅

Не надо привыкать к новому хуку. Работает он точно также, как useState.

При этом, вторым аргументом можно передавать какие-то опции, которые пока я ещё изучил не полностью.

Также, самый жирный плюс "сигнала" (стейта) в том, что при его изменении, компонент не будет каждый раз вызываться (ререндериться).

Не нравится ❌

Раздражает так называемый "реактивный примитив", который нужно каждый раз вызывать, если необходимо вытащить само значение.


_10
const [count1, setCount] = createSignal(0);
_10
_10
// ✅
_10
return <h1>{count()}</h1>
_10
_10
// ❌
_10
return <h1>{count}</h1>

Не понимаю, почему нельзя было в своём компиляторе сделать так, чтобы стейт вызывался сам?

Я хочу просто передавать переменную в JSX, как в React'е. Меня бесят эти круглые скобки.

"Не то чтобы по вкусу вкусно..."

Например, ещё вот. Хоть речь и косвенно затрагивает createSignal, но все же.

Мы привыкли в ReactJS к тому, что можно внутри функции описать логику фильтрации.


_10
const [search, setSearch] = useState('');
_10
_10
const result = items.filter(obj => obj.title.includes(search));
_10
_10
return <ul>{result.map((obj, i) => <li key={i}>{obj.title}</li>)}</ul>

В SolidJS так работать код не будем.

Напомню, что ReactJS при каждом изменении search будет вызывать наш компонент для отрисовки новых данных.

Но SolidJS вызов функции делает только при первом рендере, что в целом, я считаю хорошим решением.

И тот же самый код из ReactJS, в SolidJS будет выглядеть вот так:


_11
const [search, setSearch] = createSignal('');
_11
_11
const result = createMemo(() => items.filter(obj => obj.title.includes(search)));
_11
_11
return (
_11
<ul>
_11
<For each={result()}>
_11
{(obj) => <li>{obj.title}</li>}
_11
</For>
_11
</ul>
_11
);

Не могу сказать, что мне не нравится такое решение или наоборот. Почти история о двух стульях.

Я понимаю, что createMemo тут нужен из-за "особой" реактивности SolidJS.

Но что делать новичку, который первым фреймворком выберет SolidJS?

У нас в реакте до сих пор некоторые "синьоры-помидоры" не разобрались с useMemo / useCallback, какой ещё createMemo.

onMount, onCleanup

Тут всё нравится. Интуитивно понятные названия у хуков, которые чётко дают тебе понять, какую задачу они выполнят.

Но я бы переименовал onCleanup в onUnmount.

Пропсы

Вот тут у React-разработчиков будет бугуртить. Готовы? Точно?

Пропсы нельзя деструктурировать и ты приемный.


_10
// ✅
_10
function Article(props) {
_10
return <h1>{props.title}</h1>
_10
}
_10
_10
// ❌
_10
function Article({ title }) {
_10
return <h1>{title}</h1>
_10
}

Нифига себе, да! А ты что думал? Деструктризацию он захотел...

Кстати, у Solid'а вообще какой-то фетиш с пропсами:

  • splitProps - не печалься, есть альтернативный вариант "деструктуризации".
  • mergeProps - объединение пропсов, хз зачем даже.

📊 Сборка и производительность

Не стоит на 100% верить тем красивым графикам и скриншотам, что показывают в других статьях, рассказывая о том, как SolidJS выигрывает в производительности и как мало весит бандл приложения.

Все прочитанные мной статьи "React vs Solid" делают сравнение на каком-нибудь счётчике или тудушке.

И ирония в том, что я тоже сделал сравнение этих двух библиотек, но только на своём пет-проекте.

По факту, это такая ситуативная и спорная фигня с этими сравнениями библиотек.

При дальнейшем чтении, учитывайте, что для более конструктивной оценки необходимо проводить более детальный анализ с бенчмарками и т.п. Я же сделал более поверхностное сравнение.

Вот вам демо-ссылки одного и того же реального пет-проекте на ReactJS и SolidJS.

ReactJS: https://rgxp-react.surge.sh
SolidJS: https://rgxp-solid.surge.sh

⚖️  Размер сборки

Машина: MacBook Air, М1, 8 Гб.

SolidJS

_10
$ vite build
_10
vite v3.0.9 building for production...
_10
_10
✓ 23 modules transformed.
_10
dist/assets/favicon.0e726a38.ico 14.73 KiB
_10
dist/index.html 0.58 KiB
_10
dist/assets/index.c7ca6d80.css 8.49 KiB / gzip: 2.12 KiB
_10
dist/assets/index.0d95ce19.js 38.27 KiB / gzip: 14.23 KiB
_10
_10
✨ Done in 1.75s.

ReactJS

_10
$ react-scripts build
_10
Creating an optimized production build...
_10
Compiled with warnings.
_10
_10
File sizes after gzip:
_10
_10
59.45 kB build/static/js/main.fdba12e1.js
_10
4.1 kB build/static/css/main.49103304.css
_10
_10
✨ Done in 8.00s.

Что делал перед сборкой?

  • Использовал ту же самую JSX-разметку
  • Использовал роутер @solidjs/router и react-router-dom@6.
  • Использовал дополнительные NPM-пакеты: sass, reset-css
  • Удалил подключенный JSON-файл со списком регулярных выражений, чтобы в сборку попала только кодовая часть, без мусора.

Итоговые JS-бандлы:

  • ReactJS = 59.45 kB
  • SolidJS = 38.27 kB
  • Разница = 21.18 kB

⚙️️  Производительность

Ниже привожу отчёт по средней оценке производительности двух приложений на ReactJS и SolidJS.

Параметры оценки следующие:

  • Делал 6 раз сравнение через Perfomance
  • Во вкладке инкогнито
  • Расширения все отключены на момент теста
  • Машина: MacBook Air, М1, 8 Гб.
ReactJS

_10
Scripting: ~28ms
_10
Rendering: ~8ms

Скриншот

SolidJS

_10
Scripting: ~16ms
_10
Rendering: ~7ms

Скриншот

Итого

Я переписал 1 пет-проект на SolidJS и не сказать, что сильно впечатлён от рефакторинга.

Из плюсов:

  • JSX разметка осталась на 90% той же
  • Проект весит на 1.5 раза меньше
  • Рендеринг на 1.75 раза быстрей

Из минусов:

  • Все что я описал выше
  • 90% документации Solid, это - "Есть два стула...". Очень малым реальных примеров, большинство из которых либо не полностью тебе дают понять, как работают функции, ли описаны вне контекста.
  • Небольшое сообщество
  • Небольшой выбор NPM-пакетов для решений задач
  • Solid не для начинающих. Это больше про тех, кто уже изучил React и пришёл щупать что-то новое

ReactJS-разработчику будет несложно освоить SolidJS, учитывая его особенности.

Но только после того, как вы закончите изучать React и отточите в нём свои знания, только лишь тогда я бы советовал лезть в изучение SolidJS.

Вакансий по SolidJS пока практически нет (по крайней мере я не встречал за последние полгода).

Пока я не могу для себя назвать SolidJS идеальной заменой ReactJS'y.

Полезные ссылки:

#solidjs