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

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

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

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

  • при настройке tsconfig для Solid нужно указывать { "jsx": "preserve", "jsxImportSource": "solid-js" }

  • транспиляция Solid JSX сейчас поддерживается только с помощью babel с плагином babel-preset-solid, поэтому пришлось его подключить к esbuild, который я сейчас использую для сборки большинства проектов

  • Solid предоставляет ряд вспомогательных компонентов (<For> для списков, <Show> для показа по условию, <Dynamic> для динамических компонентов и html-тегов), поэтому в адаптерах React добавил их базовые реализации

  • для создания сторов я выбрал аналог MobX makeAutoObservable, который в Solid называется createMutable. Однако он не поддерживает автобиндинг методов класса, что решилось библиотекой auto-bind, и не поддерживает присвоение observable-объекту нового значения. В целом адаптация выглядела так:

// 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)
  • Solid JSX немного отличается от React JSX. innerHTML вместо dangerouslySetInnerHTML, onInput вместо onChange, названия свойств в style такое же, как в css (z-index вместо zIndex, border-top вместо borderTop), нельзя деструктурировать props

В целом, отличий оказалось достаточно мало, и написание адаптеров не заняло много времени. Основное время заняла конвертация архитектурных библиотек с 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, который, возможно, сможет решить эти проблемы. Однако проблему ограниченной экосистемы ввиду низкой популярности фреймворка решить будет не так просто.

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