Переосмысляя реактивность

Прошлое и будущее реактивного программирования

Bulat Khabibullin
12 min readSep 6, 2021

От автора перевода

Это перевод доклада Rethinking reactivity, в котором основатель Svelte Rich Harris рассуждает об идее реактивности и её реализации в современных веб-технологиях. Доклад прочитан в 2019 году на конференции YGLF в Израиле. Текст не является точным и полным переводом.

Урок истории

Около 50 лет назад человечество вступило в новую эру благодаря невероятному технологическому прорыву — изобретению электронных таблиц.

В 1969 году Реми Ландау и Рене Пардо создали LANPAR Technologies — компанию и программу “для работы с произвольными массивами данных”. Главной фишкой этой технологии было так называемое предварительное объявление (forward referencing), которое сегодня мы бы назвали реактивностью. Это значит, что ячейки с данными вычислялись друг из друга и обновлялись автоматически, если менялись какие-либо входные данные.

Как пример можно взять эту таблицу с фруктами:

Данные в ячейке с суммой вычисляются из ячеек с ценой и количеством, а итоговый ценник — из всех ячеек с суммой по каждой позиции

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

LANPAR Technologies опередили своё время и не были оценены по достоинству. Первыми популярными электронными таблицами стали Visicalc, появившиеся в 1979 году. Но Visicalc не умел обновлять зависимые данные. Если вы меняли данные в одной ячейке, вам нужно было вручную пройтись по всей таблице и поменять остальные. Люди пользовались этой программой в отсутствие альтернатив.

В 1983 появилась программа Lotus 1–2–3, которая вновь реализовала предварительное объявление, и это стало началом эпохи электронных таблиц, их стали использовать повсеместно. Через некоторое время про Visicalc все забыли.

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

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

Есть много определений реактивного программирования, но мне нравится вот такое:

“Суть реактивного программирования — определить динамический характер данных во время декларирования”

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

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

React принёс в мир Virtual DOM. Как он работает? Возьмём простой компонент счётчика:

В компоненте есть стейты count и name, а также две функции, которые меняют этот стейт через setCount и setName. Вот как рендерится и изменяется этот компонент:

Каждый раз, когда мы меняем данные, Virtual DOM генерируется заново, а потом React бежит по всему дереву компонентов, сравнивает и объединяет старые и новые данные. Под капотом происходит примерно следующее:

Верхний элемент был div и стал… div. В атрибутах был className=”app”, стало — className=”app”. В children элемент h1 стал… элементом h1 — оставляем. Внутри какой-то текст. Он изменился? Нет, оставляем. Элемент input. Он изменился? Нет, дальше — value. Оно поменялось? Нет, дальше. Кнопка была? Осталась та же. Текст поменялся? Да! Ура, применяем к DOM!

Вся эта работа была проделана, чтобы поменять 4 на 5…

Код объявления функций и переменных тоже запускался и отрабатывал снова и снова.

Дело в том, что для React ваше приложение — это чёрный ящик. React понятия не имеет, какие данные, как и куда движутся. Он просто реагирует на любые изменения, и в этом его главная проблема. Как бы вы ни определяли реактивность, React — худшее название для этой технологии.

Но вдруг Virtual DOM достаточно быстрый, чтобы делать всю эту работу? К сожалению, нет, и это признают сами разработчики React. Именно поэтому они дали нам такие инструменты как:

  • shouldComponentUpdate
  • React.PureComponent
  • useMemo
  • useCallback

Все эти инструменты созданы, чтобы сказать вашему коду: “На этот кусок приложения можно не тратить ресурсы”. По сути вы делаете за компьютер его работу.

Компиляторы — это новые фреймворки

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

Почему фреймворк должен работать в браузере? Может, фреймворки должны работать на уровне билда? Как сказал Кайл Симпсон:

“Компиляторы — это новые фреймворки”.

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

Посмотрите на этот код:

Самое интересное в нём — это changed.count. Как мы узнаём и сообщаем дальше, что какие-то данные поменялись?

Вот как это делает React:

Он берёт новый стейт, объединяет со старым стейтом и запускает перерисовку. Минус этого подхода в том, что он задействует this. Как только вы начинаете зависеть от this, вы получаете множество ограничений, проблемы от которых трудно осознать на старте.

Но давайте спустимся на уровень ниже. Как мы вообще сообщаем компьютеру, что данные изменились? Мы используем оператор =:

Vue.js осознали это раньше всех. Во Vue вы запускаете обновления присваивая свойствам новые значения:

Но и здесь мы получаем ограничения this.

Как решает эту проблему Svelte? Он перемещает реактивность из API компонента в язык. Вот то же самое приложение счётчика, написанное на Svelte:

Мы объявили те же переменные и реализовали те же методы. Единственное отличие — мы меняем данные напрямую, присваивая переменным новые значения.

Как это работает под капотом? Давайте посмотрим, что выдал компилятор:

Видим, что код почти не поменялся, но добавились вызовы $$invalidate(). Этот метод вызывается каждый раз, когда вы меняете значение переменной. Он говорит компоненту: “Вот эти данные изменились, имей в виду, когда будешь обновляться. И кстати, тебе надо обновиться”.

Код приложения можно немного усовершенствовать. В Svelte необязательно использовать отдельные методы для обработки событий, чтобы связать input с переменной. Мы можем использовать специальную директиву bind. А обновление счётчика запишем прямо в обработчике:

Мы все ещё не реактивны

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

В React эту проблему решать не приходится, потому что код всё равно запускается снова и снова. Рассмотрим эту проблему на примере классического to-do листа.

У нас есть список задач, которые могут быть выполнены или не выполнены, а также кнопка “Спрятать сделанное”, которая скрывает выполненные задачи. Чтобы реализовать эту фичу, мы фильтруем массив с задачами и генерим новый, а потом рисуем его с помощью map.

Теперь представим, что этот компонент находится внутри другого компонента, и тогда массив filtered будет генериться снова и снова. В этом случае мы обернём его в хук useMemo, указав зависимости todos и hideDone:

useMemo сравнивает переменные в зависимостях с их значениями в предыдущей итерации отработки кода. Если их значения не изменились, то хук возвращает закешированные данные. Это немного эффективнее, но разработчику нужно писать больше бойлерплейта.

Теперь давайте посмотрим на тот же самый код на Svelte:

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

Оператор судьбы

Думая над этой проблемой, я нашёл вдохновение в работах Майка Бостока, а если быть точнее — в продукте под названием Observable. Это очень популярный инструмент в мире работы с данными, похожий на Jupyter Notebook и другие аналогичные среды разработки. За одним важным исключением: все остальные инструменты запускают ваш код сверху вниз, а Observable запускает ваш код в так называемом топологическом порядке.

Если выражение A зависит от результата выражения B, то выражение B будет вычислено первым, вне зависимости от порядка декларирования. Прямо как в электронных таблицах.

Один австралийский разработчик написал классный пост “Что такое реактивное программирование?”. В нём рассказывается про антрополога из 2051 года, который изучает код, написанный в наше время, и не может ничего понять.

Например:

Очевидно, что переменная b задумана зависимой от a, но эта зависимость не работает. После того, как значение a изменилось, нам приходится вручную менять значение b ещё раз.

Автор пишет, что в будущем эта проблема решена с помощью так называемого “оператора судьбы” (destiny operator) — <=:

Теперь две переменные связаны. Значение b меняется вместе с a. Можем ли мы использовать “оператор судьбы” в JavaScript? К сожалению, нет. Это невалидный синтаксис. Однако в JS есть кое-что, что может помочь реализовать эту концепцию.

В JS есть так называемые “меченые выражения” или “метки” (labeled statements). Мы редко с ними сталкиваемся и ещё реже используем сами. Вот пример кода с вложенным циклом и меткой:

Мы в Svelte нашли применение меткам. $: означает связку переменных. Теперь значение b привязано к значению a:

Вернёмся к нашему to-do листу на Svelte. Заменим const filtered = … на $: filtered = …:

Значение массива filtered теперь привязано к hideDone и todos. Можем поиграться с привязками ещё немного и добавить новую переменную, которая на лету вычисляется из массива filtered:

Теперь приложение работает, как нужно

Давайте посмотрим на результат компиляции этого кода:

Svelte взял выражения со связанными переменными и обернул их в условные конструкции. Обратите внимание на порядок. Svelte знает, что showing зависит от filtered, поэтому filtered вычисляется раньше. Это и есть так называемый топологический порядок, о котором мы говорили ранее. В результате получается очень быстрый эффективный код.

Магия — светлая или тёмная?

Когда люди впервые видят, как работает Svelte они называют это магией, имея в виду тёмную магию. Создатель Vue.js Эван Ю сказал, что данную технологию стоило бы называть SvelteScript. И он прав. Svelte — это уже не совсем JavaScript. Мы используем язык для фана и профита.

Мне немного странно слышать это от Эвана, потому что именно он начал продвигать идею о том, что присваивание новых значений должно запускать сайд-эффекты. Эван различает светлую и тёмную магию в зависимости от того, где она происходит — в браузере или при компиляции.

— Мне кажется, что запускать обновления при присваивании — это всё-таки слишком.

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

— Ха-ха. Думаю, речь о разнице межу “это технически возможно в рамках ограничений языка” и “это невозможно, если только не пошаманить немного с компиляцией”. Любопытно, чем это закончится!

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

Производительность и бенчмарки

Теперь поговорим о производительности и о том, как её измерять. По этой картинке можно увидеть, что Svelte в 35 раз быстрее React и в 50 раз быстрее Vue.

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

Тестовое приложение на React для демонстрации Concurrent Mode

Несколько лет назад в своём докладе о Concurrent Mode в React он показывал приложение, которое имитирует нагрузку на DOM. Чем дольше юзер набирает текст, тем глубже и шире становится DOM-дерево, и тем больше становится задержка между набором текста и его отображением в поле ввода. Суть демки была в том, чтобы продемонстрировать радикальный прорыв в производительности, который даст релиз Concurrent Mode. Но даже в асинхронном режиме приложение очень скоро начинает тормозить.

То же приложение на Svelte

То же приложение, сделанное на Svelte, демонстрирует 0 задержек. Встроенный в приложение измеритель недовольства юзера (frustration meter) никогда не краснеет и даже не желтеет. С моей точки зрения, единственный способ гарантировать хороший пользовательский опыт — это работать на невероятно быстрых технологиях. Чтобы почувствовать разницу, можно пройти по ссылке и поиграться с демкой самому.

Рассуждая на эту тему, я очень люблю использовать пример двигателя внутреннего сгорания. Современные ДВС — это потрясающие устройства, настоящее чудо инженерной мысли. Автомобильные компании тратили миллионы долларов, год за годом совершенствуя свои детища. Но потом пришла Тесла и сказала — а что, если мы пересмотрим базовую идею? Что, если мы откажемся от ископаемого топлива?

Средний ДВС состоит из сотен движущихся частей, а электродвигатель — из двух. Лучшие ДВС в мире имеют КПД примерно 20–25%, рядовой электродвигатель — 85%. Можно годами жить и успешно развиваться в рамках определенного набора ограничений, но рано или поздно игра меняется.

Как и зачем ускорять код

Существует несколько способов ускорить свой код:

  • Разбить на маленькие кусочки и хорошенько их организовать
    Станет немного лучше, но в целом будет то же самое.
  • Использовать веб-воркеры
    Какое-то время эта идея казалась всем отличной, но по итогу выяснилось, что оно так не работает. Нельзя рассовать код по разным местам и ждать какого-то прорыва.
  • Переписать всё на Rust
    Один парень по имени Ник Фитцджеральд написал Virtual DOM на Rust. Он заявил, что добился лучшей производительности на рынке. Если кто-то говорит такие вещи, то я иду и проверяю это. Я пришёл и добавил Svelte в его бенчмарк. К сожалению, Svelte выдал лучший результат. Ник проделал хорошую работу, но это всё ещё Virtual DOM…

На самом деле существует всего один верный способ ускорить код — избавиться от него. Давайте будем как Мари Кондо и честно спросим себя: “Этот код вызывает радость?”.

Когда я говорю о производительности, я слышу упрёки, что витаю в облаках и решаю выдуманные проблемы. Я с этим не согласен. Производительность — это самая жизненная проблема, которую можно представить.

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

  • Stone — бразильская финтех-компания, которая выбрала Svelte, чтобы писать интерфейсы для терминалов оплаты. Производительность была ключевым фактором, потому что приложение работает на машинках с гораздо более слабой батареей и вычислительной мощностью, чем любой современный смартфон.
  • Mustlab — российская компания, выпускающая приложения для Смарт-ТВ. Для них вопрос производительности также стал ключевым.
“Десктоп устарел 💀, мобилка устала 😴, встроенный веб — заряжен 😎”

На мой взгляд, это не частные случаи, а нарастающий тренд. Раньше передовая веб-технологий лежала в браузерах, позже — на экранах смартфонов и планшетов, но будущее — это встроенный веб. Носимые устройства, интернет вещей, автомобильные компьютеры. Жадные JS-фреймворки не подходят под эти задачи.

Другие фишки

Доступность

Svelte будет ругаться на вас, если вы игнорируете доступность своих интерфейсов. Например, Svelte предупредит вас, если вы забудете атрибут alt для тега <img>. Он позволит вам писать разметку без учёта доступности и скомпилируется, но не будет вас за это уважать.

Стили

Стили — неотъемлемая часть веб-приложений, однако ни один фреймворк почему-то не касается стилей вообще. В Svelte мы пишем стили прямо в теге <style> внутри компонента. Стили задаются тегам напрямую. Не нужно писать классы и использовать сложные схемы их именования типа БЭМ.

Несмотря на то, что вложенный компонент тоже содержит тег <p>, стили не конфликтуют. Svelte генерит уникальный хэш и добавляет его элементу как класс.

Ещё одна интересная особенность, что если в проекте есть неиспользуемые CSS-директивы, то мы получаем предупреждение, а сами стили не попадают в итоговый код. Знаете это чувство, когда вы боитесь трогать стили, чтобы ничего не сломать в проекте? Забудьте про эту проблему.

Анимации

В Svelte есть набор встроенных анимаций на чистом CSS для элементов типа уведомлений или карточек, которые нужно перемещать, скрывать и показывать. Их приятно писать и читать. Они сильно облегчат вам жизнь. Другие фреймворки не делают подобных фишек, потому что переживают за размер пакета. Иногда размер пакета становится чуть ли не уникальным торговым предложением.

Но Svelte — это компилятор, который не страдает от таких ограничений. Если вы не используете конкретную фичу, то кода этой фичи просто не будет в проекте. Вы всегда получаете необходимый минимум. Это позволяет нам добавлять во фреймворк сколько и чего угодно.

За рамками Svelte

Команда Svelte занимается ещё несколькими проектами. Вот они:

  • Sapper
    Аналог Next.js в мире React. Роутинг, ССР и другие полезные вещи.
  • Svelte Native
    Разработка мобильных приложений на Svelte. Делается силами комьюнити.
  • Svelte GL
    Библиотека для создания 3D графики. Как Three.js.

Вместо заключения

Хочу ещё раз сказать про электронные таблицы, в которых поистине реализована концепция реактивности. Они прекрасны и мы должны стремиться к их простоте и функциональности.

Моя жена не программист, и после 15 лет жизни со мной она окончательно отказалась от идеи стать программистом. При этом она может вытворять невероятные вещи с электронными таблицами.

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

Это и есть основная идея Svelte. Сделать разработку веб-приложений максимально доступной без ущерба для возможностей, которые даёт технология.

Моя подруга, работающая в Spotify, использовала Svelte для одного проекта. И рассказала, что её менеджер, который знает немного HTML, смог написать код на Svelte, и это не было так страшно как чистый JS или JSX.

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

От автора перевода

Спасибо, что прочитали. Круто, если этот перевод принесёт хоть какую-то пользу сообществу. Особенно тем, кому пока не очень удобно слушать такие доклады с субтитрами.

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

Пишите письма:

--

--