Solid.js как альтернатива (P)React+MobX на практике

Как известно, у Solid довольно скудная экосистема, поэтому для сложных проектов я беру React+MobX. Однако недавно подвернулся небольшой mobile-only проект, в котором разве что маскированные инпуты и кастомные селекты, которых для Solid предостаточно. При этом требования к размеру выходных файлов и перфомансу были высокие.

Очевидным решением посчитал взять Solid, заодно и сравнить его по всем параметрам (размер, перфоманс, возможности реактивности, удобство настройки) в реальном проекте. Никаких синтетических тестов с рендерингом больших таблиц и хранением в сторе нескольких мегабайт данных не будет, зато приведу замеры из реального приложения. Бонусом - репозиторий с универсальной архитектурой для Solid+Preact+React, где замена фреймворка (набора стейт-менеджер + рендеринг UI) производится одной строчкой кода.

Создание кросс-фреймворкового проекта

Первым делом при изучении Solid мне было интересно, какие возможности из привычного для меня стека он поддерживает, а также сравнить side-to-side с ним без модификации кода проекта, кроме файла с адаптерами. Основные моменты, которые потребовались для создания универсального проекта:

// mobx
constructor() { makeAutoObservable(this, undefined, { autoBind: true }) }

// solid
constructor() { return createMutable(this) }
autoBind(store)


// mobx
store.object = newObject

// solid
modifyMutable(store.object, produce(state => {
  for (const key in state) delete state[key];
  Object.assign(state, newObject);
}))


// mobx
method() { this.param = 1 }

// в Solid нет action, поэтому везде ручной батчинг
method() { batch(() => { this.param = 1 }) }


// mobx
const disposer = autorun(fn)

// в Solid нет disposer, эффекты автоматически удаляются при размонтировании
createRenderEffect(fn)

В целом, отличий оказалось достаточно мало, и написание адаптеров не заняло много времени. Основное время заняла конвертация архитектурных библиотек с React+Mobx на Solid (роутинг, подключение ViewModel, работа с асинхронными функциями и т.п.). Хотя сам перевод библиотек выполнялся буквально парой строк, с полным сохранением публичного api пакетов, настройка тестов оказалась довольно неудобной.

Для тестирования компонентов Solid в настоящее время поддерживает только Jest и uvu, в связи с чем вместо стандартных зависимостей

  "mocha": "10.4.0",
  "sinon": "17.0.1",
  "chai": "4.4.1",
  "@testing-library/jest-dom": "6.4.2",
  "@testing-library/react": "15.0.1",
  "global-jsdom": "24.0.0",
  "jsdom": "24.0.0",

пришлось настраивать Jest и babel

  "@babel/core": "7.26.10",
  "@babel/preset-env": "7.26.9",
  "@babel/preset-typescript": "7.27.0",
  "@solidjs/testing-library": "0.8.10",
  "@testing-library/jest-dom": "6.6.3",
  "babel-jest": "29.7.0",
  "babel-preset-jest": "29.6.3",
  "babel-preset-solid": "1.9.5",
  "chai": "5.2.0",
  "jest": "29.7.0",
  "jest-environment-jsdom": "29.7.0",
  "jsdom": "26.1.0",
  "regenerator-runtime": "0.14.1",
  "sinon": "20.0.0",
  "solid-jest": "0.2.0",
  "ts-jest-resolver": "2.0.1",

Благо, что тесты практически не пришлось менять - разве что у Solid нет функционала rerender для компонента, чтобы протестировать сценарий изменения props. Для этого нужно создавать внешний сигнал (в MobX это был бы внешний observable).

Архитектурные ограничения

Для простоты я буду далее называть получившийся "кросс-фреймворковый проект" Реактосолидом.

В прошлом разделе я перечислил ряд синтаксических различий при написании JSX и реактивных сторов, однако есть и архитектурные ограничения, если требуется писать универсальный код, работающий в нескольких фреймворках.

1. Solid createMutable, безусловно, не полноценная замена MobX. Он не поддерживает Set и Map и в целом довольно нестабильный. Да, можно использовать Solid+MobX для получения бескомпромиссной реактивности и стабильности, однако ряд преимуществ в плане размера файлов и перфоманса пропадет.

2. В Solid нет controllable inputs. Это создает массу неудобств при работе с инпутами - особенно при обработке вводимых данных. Issue открыт давно, но предлагаемые решения с контролированием положения каретки и ручным ререндером довольно проблемные. Поэтому в Реактосолиде создать полноценный адаптер для инпутов не получилось.

3. Нельзя использовать специфичный для фреймворка функционал внутри компонента (onMount/createSignal/createMemo у Solid и хуки useState/useMemo/useEffect и т.п. у Реакта). Они не являются прямой заменой друг друга и даже с помощью адаптеров не удастся добиться полной совместимости. Для меня это не стало ограничением, т.к. я использую ViewModel подход и не пользуюсь подобным функционалом

class VM implements ViewModel {
  constructor() { return createMutable(this); }
  
  // React class-component equivalent: componentWillMount
  beforeMount() {}
  
  // React class-component equivalent: componentDidMount
  afterMount() {}
  
  // React class-component equivalent: componentWillUnmount
  beforeUnmount() {}

  reactiveData = 1;

  // computed getters
  get param() { return this.reactiveData + 1 }

  // user action handlers
  handleClick() {}
}

function App() {
  const { vm } = useStore(VM);

  return <div />;
});

4. Нельзя использовать динамическую логику внутри компонентов

// было
function Component(props) {
  if (props.isLoading) return <Loader />
  
  return <div />
}

// стало
function Component(props) {
  return (
    <Show when={!props.isLoading} fallback={<Loader />}>
      <div />
    </Show>
  )
}

Я считаю, что это более правильный подход, чем в React, в котором внутри функции компонента можно писать лапшу из хуков и вычисляемых параметров. Логика должна группироваться во ViewModel и там же оптимизироваться с помощью computed геттеров, а тело функции оставаться чистым - только подключение ViewModel и jsx разметка. Это же способствует эффективному и чистому тестированию.

Разумеется, если писать на 1 фреймворке (например, только на Solid), то ограничений будет меньше, но мне для side-to-side сравнения потребовалось привести все к общему знаменателю и отсечь лишнее, заодно и наработал практику миграции проекта с одного фреймворка на другой.

Сравнение размера и производительности

В "чистом" виде (1 компонент + 1 стор) разница в размерах колоссальная. React и Preact (compat) в таблице везде считаются вместе с MobX и mobx-react-lite / mobx-preact.

Solid

Preact

React

dev

38.31 kb

237.03 kb

1269.26 kb

dev min+br

6.52 kb

33.90 kb

128.13 kb

prod

35.26 kb

227.41 kb

808.35 kb

prod min+br

5.78 kb

29.84 kb

78.99 kb

В реальном проекте с десятком страниц и полноценной архитектурой разница тоже довольно ощутима.

Solid

Preact

React

prod

551.60 kb

756.51 kb

1346.44 kb

prod min+br

65.00 kb

88.53 kb

134.68 kb

По производительности кода в реальном проекте ситуация следующая (измерения проводились в Chrome Profiler). Первая загрузка:

Solid

Preact

React

First full render

42 ms

59 ms

71 ms

Выполнение определенного пользовательского сценария, с переходом на несколько страниц, вызовом модальных окон, показом загрузчиков и т.п., не считая первой отрисовки страницы:

Solid

Preact

React

Scripting

113 ms

185 ms

374 ms

Rendering

71 ms

97 ms

113 ms

Painting

30 ms

46 ms

49 ms

System

220 ms

289 ms

307 ms

Так как весь код, кроме адаптеров - универсальный, сравнение должно быть достаточно точным. Однако есть и ряд неточностей - например, если на React писать в привычном многим формате с вычислениями в функции рендера и хуками, то результаты у него будут хуже. Если же оптимизировать компоненты для React+MobX, то есть выносить условно Table + TableRow + TableCell и в props передавать целые observable объекты для достижения точечной реактивности, то результаты будут чуть лучше. Но архитектура Реактосолида "усредняет" результаты - не используются все преимущества и не скрываются недостатки конкретного фреймворка, так что порядок цифр должен сохраниться.

Выводы

Здесь все предсказуемо. Для проектов, где экосистема не сильно важна, и при этом важен перфоманс (лендинги, mobile-only приложения, бизнес-сайты "визитки", личные проекты и т.п.) однозначно можно брать Solid. А при умении его правильно "готовить" можно буквально за пару часов перевести на React/Preact и получить огромную экосистему. Можно также попробовать связку Solid+MobX для получения бескомпромиссной реактивности и еще более простого перехода.

У Solid есть неоспоримые архитектурные преимущества - например, точечная реактивность (можно хоть всю разметку держать в 1 файле - перерендерится только конкретный элемент, читающий реактивные данные). Это позволяет выделять компоненты только по семантическому признаку, а не ради перфоманса. Подход "функция компонента вызывается только 1 раз" тоже очень удобен и убирает из проекта бойлерплейт и правила, навязываемые React-хуками. Также отсутствует необходимость оборачивать компонент в MobX observer и вручную синхронизировать props с ViewModel, что упрощает сборку, линтинг и улучшает перфоманс.

Но есть и недостатки - отсутствие контролируемых инпутов, нестабильность и ограниченный функционал createMutable. Тем не менее, Solid активно развивается, и ведется работа над Solid 2.0, который, возможно, сможет решить эти проблемы. Однако проблему ограниченной экосистемы ввиду низкой популярности фреймворка решить будет не так просто.

Приглашаю смотреть Реактосолид для развлечения и собственных тестов.

@DmitryKazakov8
08.05.2025 23:25 UTC
Первоисточник

Комментарии

@TheHost
08.05.2025 22:17 UTC
0

Solid хоть и пионер по части сигналов, но мне больше зашел Alpine.js с adonis и edge

@DmitryKazakov8
08.05.2025 22:29 UTC
0

Мне была важна возможность перехода на React/Preact+MobX если не хватит экосистемы, поэтому детали реактивности были не важны - главное, чтобы была возможность делать реактивные инстансы классов по аналогии с MobX. Что под капотом - геттеры-сеттеры, прокси или сигналы не так важно, если они дают достаточный перфоманс и не налагают серьезных ограничений.

Конечно, есть фреймворки, предлагающие менее 5.78 kb на старте, но не с jsx синтаксисом и без возможности практически безболезненно переключаться на другие фреймворки.

08.05.2025 22:34 UTC
0

А Vue не пробовали? Это из коробки как раз React + Mobx ну и плюс остальные плюсы самого vue)) Alpine кстати с vue реактивностью

08.05.2025 23:44 UTC
0

Другая экосистема, другой хаб для дискуссии)

@izibrizi2
09.05.2025 00:35 UTC
0

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

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

Зачем вам контроллируемые инпуты? Это реактовское понятия, как раз связанное с ререндерами, чтобы не словить рассинхрон стейта. В солиде такой пробоемы в принципе нет.

@DmitryKazakov8
09.05.2025 01:48 UTC
+1

Некст вне контекста статьи, как и мегабайты картинок. Контролируемые инпуты нужны для фильтрации того, что вводит пользователь. Попробуйте на нативном инпуте нативно обработать oninput, onchange, onpaste, onkeypress, onkeydown c учетом мобильных браузеров и всех версий операционки и разрешить скажем ввод "a-b". Это очень нетривиальная задача, если у вас в наличии десятки устройств.

Добавьте обработку маски и позицию каретки для удобного редактирования.

09.05.2025 11:47 UTC
0
09.05.2025 13:43 UTC
0

Да, есть немало библиотек, в том числе для Solid, которые решают эту проблему. Но если в приложении пара инпутов и не хочется раздувать размер бандла, то в контролируемых инпутах Реакта все решается намного проще, чем в неконтролируемых Солида.

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

09.05.2025 18:15 UTC
0

Я всё таки не могу понять каким образом обработка картетки и прочие манипуляции с инпутом зависят от реакта. Или это солид не перехватывает какие то события? Каким образом контролируемый инпут помогает двигать курсор?

11.05.2025 20:14 UTC
0

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

@isumix
09.05.2025 06:36 UTC
-1

Ох, если вы бы помогли "имерить" также размер и производительность приложения на Fusor, я был бы вам очень вам признателен. Уверен, что результаты были бы не худшими, а может и лучшими. Все руки не доходят до этого.

@supercat1337
09.05.2025 06:45 UTC
0

А вы попробуйте сделать пару-тройку примеров fusor+mobx. И уже тогда будет более понятно. Размеры бандлов считать не так сложно. А остальные данные из devtools просто берутся.

@Vlad_IT
09.05.2025 09:40 UTC
+1

Спасибо за статью и за то, что про перф что-то пишите, очень важная тема!

Меня смущает, что Painting так отличается. Если у вас одинаковое число элементов и одинаковые стили, то он должен +- быть равным, и Rendering тоже.

Я погонял локально ваш проект, и у меня всегда при перемещении по Page1 - Page 2 метрика Paiting на всех сборках близка к 5мс, а Rendering 15мс. Различий между фреймворками не заметил.

Плюс хочу добавить, что для меня супер сборка это Preact+Signals. Там перф ререндеров получается такой же, как с SolidJS. Если на них писать без использования useState/Callback/Memo, то можно вообще все приложение без ререндеров сделать (т.к. Preact если в div получает сигнал, то он не вызывает ререндер, а просто меняет атрибут элемента по изменению сигнала, по сути как в solid). Плюс совместимость с библиотеками React есть.

@DmitryKazakov8
09.05.2025 13:37 UTC
+1

Да, меня тоже смущает, что Painting отличается на 50%. Думаю, тут дело в ререндерах и отсутствии оптимизации для Реакта (если бы выносил больше компонентов и они точечно обновлялись - то меньше бы было), об этом написал в статье.

Page1 - Page 2 не реактивные, а прод-приложение, из которого приводил метрики - реактивные, там есть изменения в структуре разметки, динамические элементы, показы по условию, изменения стилей и т.п. Делать что-то сложное в Реактосолиде я не стал - просто сгенерировал статику через AI и убрал 80% архитектуры прод-приложения. Можно самостоятельно для интереса сделать что-то более сложное - думаю, различия в перфомансе будут более явными.

Preact+Signals, к сожалению, совсем не поддерживает реактивные классы и завязывается на написание логики внутри компонента - этот подход не адаптировать под ViewModel и MobX. Перфоманс, уверен, будет лучше, но и ограничения явные - сильная привязка к Preact и невозможность быстрого переключения на React/Solid. Для меня же Solid был новым опытом, и оставить себе "путь отступления" было крайне важным на старте - хотя после написания проекта это оказалось избыточным, экосистемы и возможностей Solid хватило с лихвой.

09.05.2025 14:14 UTC
+1

Preact+Signals, к сожалению, совсем не поддерживает реактивные классы

Классовые компоненты да, там с костылями только. Я и команда не пишем новый код на классовых компонентах, а в старом коде можно хелпер создать, который будет подписываться на сигнал и делать forceUpdate. Но это велосипед, поэтому ваша правда.

и завязывается на написание логики внутри компонента - этот подход не адаптировать под ViewModel и MobX.

Я делал ViewModel (правда там нет автоматической реактивности, нужно помечать поля). Реактивность работает за пределами preact, сигналы можно как угодно формировать, не обязательно в компонентах. А если такой сигнал уже подцепится в рендере компонента, то компонент автоматически становится observer компонентом.

т.е. в компонентах создаются сигналы через useSignal, а за пределами (в сторах) уже через signal.

Да, нет магии с makeAutoObservable, но на мой взгляд это только плюс, makeAutoObservable это код с кучей оверхеда.

Другой вопрос, что сигналы это примитивы, поэтому observable.deep там невозможен из коробки (но есть открытые решения).

сильная привязка к Preact

С React тоже отлично работает. А в SolidJS свои сигналы, которые очень похожи на преактовские.

В любом случае, в новых проектах проще SolidJS использовать. Preact+Signals хорош там, где уже есть react/preact приложение, или это проект на большую команду разработчиков, куда проще искать react разработчиков чем solidjs (да, при найме это оказывается важно, хотя обе технологии простые, но люди хотят строчку в резюме "работал с реактом").

09.05.2025 15:04 UTC
0

С React тоже отлично работает

У Реакта другая система реактивности - изменения Preact signals не будут вызывать ререндер компонента, а если с адаптером - то все равно не будет точечных апдейтов без ручной оптимизации. То есть на мой взгляд привязка к Preact будет достаточно сильная.

Для осуществления глубокой реактивности (не только примитивов), как вы правильно заметили, тоже будет необходима дополнительная библиотека, и по размеру код думаю приблизится к Preact+MobX, учитывая все нюансы.

Не спорю, что если использовать только 1 фреймворк, то можно оптимизировать код, однако мне было интересно именно side-to-side сравнение, поэтому взял MobX для связки с Preact.

@In4in
09.05.2025 15:02 UTC
0

Хз как React вообще еще жив в 2025-ом. Когда рядом есть Vue, ушедший на несколько лет вперед...

@markelov69
12.05.2025 09:13 UTC
+2

Но не ушедший от react+mobx, который доступен с далекого 2016 года.