Всем привет, меня зовут Артём Арутюнян, и я уже пять лет изучаю реактивное программирование. Меня задела недавняя статья, Big State Managers Benchmark, в которой моя библиотека Reatom заняла лишь третье место (скорее второе, ну да ладно) и я решил написать самую эффективную реализацию реактивных состояний, убрав лишние фичи, сфокусировавшись на простоте и производительности.

Немного поэкспериментировав я добился удивительных результатов, в сто строк (0.3KB gzip) уместив максимально простое апи, которое позволяет подключаться к React и Svelte без дополнительных адаптеров. Но самое главное, найденный алгоритм фундаментально покрывает любые краевые случаи условных переподписок зависимых вычислений, с которыми подавляющее большинство популярных библиотек не справляется и дают глитчи.

Если вам интересны детали реализации — прошу под кат.

Идея


Сразу выложу карты на стол и опишу ключевую оптимизацию, а потом разберём алгоритм поэтапно. Промежуточные вычисляемые зависимости между подписчиками и мутируемыми листьями графа не обязаны иметь и инвалидировать ссылки на другие компьютеры, которые их используют — достаточно иметь список своих зависимостей для простой мемоизации. Для распространения изменений подписчики должны храниться напрямую в мутируемых листовых нодах. Иначе говоря, граф вычисляемых зависимостей можно сделать не двунаправленным, а однонаправленным, что сильно сокращает расходы на его инвалидацию (распространение изменений и обновление). Двунаправленными остаются только краевые части графа.

Читаемые исходники с наглядной типизацией и комментариями можно найти тут.

Текущая ситуация


Удивительно, что в 2023 мы открываем новые границы производительности реактивности. Не первый раз убеждаюсь, что тема эта для нас новая и не изученная. Можно понять, реактивность ортогональна компайл-тайму, ведь её смысл — вынесение связей модулей из статического описания в рантайм. Думаю, поэтому за неё ещё не брались серьёзно «большие дядьки», которые предпочитают копаться в компиляторах. Идея шатать графы в рантайме на каждый чих кажется безумной. Особенно учитывая количество необходимой памяти на каждую ноду — двунаправленные связи, флаги дертичекинга, кеши — всё это может потреблять ресурсов заметно больше обслуживаемых вычислений. Стоит оно того?

Ещё год назад фронтом производительности среди реактивных библиотек был пятилетний CellX, потом на сцену вышел Preact со своими сигналами и неплохо так всех порвал. Хотя из статьи по первой ссылке вы могли узнать, что реактивность в преакте глючная и может пропускать некоторые вычисления с некорректными данными. И вот, я представляю вам венец корректности и оптимальности реактивных состояний — Act. У каждой ноды всего один список зависимостей, и интеджер для тривиального кеширования, это практически всё.

Стоит подчеркнуть, что Act — самая оптимальная библиотека в вопросах скорости создания связей, обновления и размера бандла, а также простоты внутренней реализации. Конечно, есть случаи, в которых Act будет не самым быстрым решением, а результаты одних и тех же бенчмарков могут отличаться от движка к движку. Во второй половине статьи мы разберём возможные трейдофы.

Алгоритм


▍ Пример


Для подробного разбора давайте возьмём реальный пример и определимся с терминологией.

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

export const currencies = act({ USD: { EUR: 1.1 }, EUR: { USD: 0.9 } });

export const currency = act("USD");  

export const products = act([]);

export const sum = act(() =>
    products().reduce((acc, { price, count }) => acc + price() * count(), 0)
);

export const createProduct = (dto) => ({
    ...dto,
    count: act(1),
    price: act(() => currencies()[currency()][dto.currency] * dto.cost),
});

export const addProduct = (dto) =>
    products([...products(), createProduct(dto)]);

▍ Граф


Реактивные связи образуют граф, края которого, с одной стороны mutable / writable / base / value / leaf (currencies, currency, products, count), а с другой стороны подписчики / subscribers / listeners / effects, которые являются потребителями вычислений. Нейминг в разных библиотеках отличается, но промежуточные вычисляемые ноды (sum, price) практически везде называются computed или combine, главная сложность кроется именно в них.


Так как в вычисляемую ноду может заходить сразу несколько связей, а из базовой ноды может исходить несколько связей, мы получаем ациклически направленный граф (DAG), который отличается от дерева (одной из самых простых и эффективных графовых структур) тем, что во время его наивного обхода есть риск несколько раз зайти в одну и ту же ноду, что может быть чревато лишними вычислениями. Вот описание рекурсивного обхода в глубину (DFS) зависимых нод в случае смены currency:


Сама картинка страшно не выглядит, но стоит учитывать, что это не отображение графа зависимостей, а отображение процесса его обхода, в котором мы дважды заходим в sum и первый раз «приносим» новое значение price1 ещё не обновив price2 из-за чего до нотификации price2 --> sum сумма будет высчитана неверно, что в некоторых системах может породить ошибки представления или даже вычисления (throw TypeError). Но если ошибок можно избежать, лишние вычисления будут требовать лишних накладных расходов, нас же интересует возможность автоматической оптимизации и отбрасывания лишнего. Это классическая проблема для топологической сортировки и её разновидностей: можно помечать ноды «посещёнными» (классический dirty checking), можно рассчитывать для каждой ноды веса (вес тем больше чем позже нода была создана) и сортировать по ним релевантные ноды графа до его обхода. Можно придумать десяток комбинаций разнообразных приёмов из теории графов. Но в продвинутых системах, где реактивность старается быть прозрачной и позволяет гибче её описывать, используя нестатическое апи combine($a, $b, (a, b) => a + b), а динамическое computed(() => a() + b()), появляются ДИНАМИЧЕСКИЕ ГРАФЫ.


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

Так зачем оно? Дело в удобности конечного апи. Любые либы с мутабельным стейтом под Proxy позволяют использовать свойства объектов и прозрачно подписываться на их изменения — это удобно для разработчика приложения, но совсем неудобно для разработчика библиотеки, т. к. убирается явное использование его методов, чтение и подписки просто происходят в произвольном порядке внутри функции пользователя. Какие условия там могут быть описаны, неизвестно, и статически не могут быть проанализированы: ифы, тернарки, форы и вайлы, колбеки. Хотя тот же switchMap из декларативного Rx позволяет менять структуру графа, чтобы убрать из рыксы глитчи придётся тоже поднапрячься. Чтобы кешировать переподписки между вызовами пользовательских вычисляемых функций, применяется счётчик вызовов, который позволяет просто сравнивать предыдущий и новый список используемых зависимостей и на основе этого принимать решение о инвалидации подграфа зависимостей… Ух, ещё не устали? Может, визуализации не хватает? А давайте остановимся и не будем погружаться во все вариации того что, как и зачем нужно делать в реактивных графах и какие сюрпризы там скрываются, это тема как минимум на серию постов. Есть множество готовых библиотек, чей код можно поизучать. Отойдём немного назад. Поэтапно погружаясь в проблемы и фичи реактивных графов, мы строили новые предположения на основе предыдущих, но что если нам вовсе не нужно кеширование и тогда, не нужна его инвалидация?

▍ Выворачиваем связи


О чём нужно задуматься, кому вообще нужны эти вычисления? При ленивом подходе ответ очевиден — подписчикам. Но если подписчик — источник истины о необходимой структуре зависимостей, а изменения могут начинаться и распространяться только в определённом типе нод — мутабельных листьях графа, зачем нам вообще хранить и инвалидировать связи в промежуточных нодах? С одной стороны логично, что если у нас уже есть граф и нам нужно связать его разные края — нам нужны двунаправленные связи, это примитивное решение для деревоподобных (tree-like) структур, но у нас же уже есть DAGи в производных нодах и это всё динамическое! Учитывая, что в нашей системе точки входа и выхода чётко разделены и находятся по краям — можно соединить только эти края и оставить в покое все замкнутые внутренние ноды.


Стало проще, да? Кешировать только sum уже намного проще. Если рисовать все связи, то было/стало выглядит так.

push (дерти чекинг) + pull (кеш) связи


notifications (сайд-эффекты) + pull (кеш) связи


Связей в полтора раза меньше и эта разница будет уменьшаться с ростом числа продуктов! Как видно, кеш связи (pull) остались — это простая мемоизация — когда у вычисляемой ноды пытаются получить значение, она сначала проходится по старому списку зависимостей и сравнивает последнее полученное состояние зависимости и текущее, если что-то различается — переданную пользователем функцию нужно перевызвать, иначе отдаём свой старый стейт. Важно отметить, что для предотвращения инвалидации вычисляемых нод по несколько раз (например, если бы currencies был вычисляемым) мы храним версию текущего обхода в каждой ноде и сравниваем при каждом заходе — это максимально простой и легковесный кеш, который очень просто инвалидировать просто инкрементом глобальным счётчиком обходов.

▍ Трейдофы


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

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

Уточню про инвалидацию связей динамического графа, это же самая большая проблема. При добавлении нового товара, в зависимости от его очереди в общем списке и используемым алгоритмам сравнения списка зависимостей в двунаправленном графе, скорее всего, придётся пересоздавать одни и те же связи или плодить достаточно много промежуточных кешей уникального списка (Set) — оверхед. В случае с notification + pull подходом нам нужно инвалидировать меньше связей, но Act вообще никакие сравнения не делает, всё проще.

В современном мире не всегда память — узкое место. Мобильные телефоны — критический сегмент рынка — используют SoC, которые часто имеют хорошую шину между памятью и процессором. Иногда лишние вычисления могут стоить дороже выкидывания старой и использования новой памяти. В Act, когда краевая нода обновляется — она пушит в общую очередь нотификации список своих зависимостей и создаёт его заново — пустой. Когда каждый подписчик будет затягивать новые значения, он неизбежно зайдёт в обновлённую ноду и добавится в новый список зависимостей. Всё. Никаких сравнений и дополнительных кешей — просто пересоздание части графа from scratch. Преимущество такого подхода в том, что при добавлении новой ноды (нового товара, например) подписчик не делает никакой новой работы — он также проходит по всему графу и добавляется ко всем заново. А как он добавляется заново к старым нодам? В них используется Set для списка сайд-эффектов, к счастью, в JS это ordered linked list с константной сложностью доступа.

▍ Ленивые отписки могут привести к утечкам памяти


Но есть в этом алгоритме потенциальная возможность для утечки памяти. Как происходят отписки? Подписчик проходится по всему графу и удаляет себя из краевых нод — не максимально оптимально, но терпимо. А если мы удалили товар из корзины? Если ссылка на товар полностью исчезает — GC её съест вместе со списком подписчиков. Но если товар переносится в список избранного, то ссылка на старого подписчика для sum там всё ещё остаётся и будет очищен только при обновлении товара и переинвалидации списка. Словить такую ситуацию не просто и тем более заставить это линейно течь, но мы же хотим не заставлять думать пользователя библиотеки о графах и их инвалидации самому, поэтому давайте исправим это. Пусть при добавлении в список подписчиков краевой ноды (writable) подписчика, самому подписчику сохраняется краевая нода в простой массив. Тогда, при инвалидации подписчика (обновлении списка продуктов) он может пройтись по старому списку краевых нод и удалить себя (благо Set.prototype.delete константный), а потом собрать заново, как говорилось выше. Обновлённый граф немного сложнее.


Но он всё ещё легче и эта реализация всё ещё производительней других решений в большинстве случаев.

Итоги


По ссылке на бенчмарк из первой статьи теперь можно найти и Act, чаще всего он показывает наилучшие результаты, а главное, работает максимально корректно и не содержит ошибок инвалидации списка зависимостей. В моём бенчмарке акт тоже показывает, в общем, лучшие результаты.

Интересно, то что этот notification-pull алгоритм с пересозданием нод очень простой по сравнению с классическим дерти чекингом, двунаправленными связями и их инвалидацией, размер бандла тому в подтверждение. Хотя после всех допиливаний и улучшений читаемости 0.3KB gzip превратились в 0.4KB brotli, но результат всё равно впечатляющий.

Надеюсь, что эта статья вскоре станет устаревшей, потому что мы найдём новые простые и эффективные способы оптимизации наших библиотек и приложений. В любом случае хочется порекомендовать попробовать Act, благодаря размеру он может прекрасно помочь добавить интерактивность на лендинг и как уже говорилось в начале статьи — он легко интегрируется с React и Svelte без дополнительных адаптеров, предсказуем и очень прост в использовании. Ну а если вам нужно решение с ещё большими перспективами по стабильности и масштабированию (лайфсайкл хуки, продвинутый дебаг, контекстно-зависимые процессы) — reatom.dev, там апи не сильно сложнее и есть уже приличная экосистема.

Играй в наш скролл-шутер прямо в Telegram и получай призы! ????️????

Комментарии (30)


  1. oskal
    01.02.2023 19:19

    апер молодец


  1. markelov69
    01.02.2023 19:43
    +1

    Опять не используются getter/setters для автоматических подписок/отписок. Опять вынужденное загрязнение кода ненужными конструкциями на ровном месте. Опять до MobX как раком до Китая.


    1. RubaXa
      01.02.2023 21:51
      +1

      Эээ, автора не защищаю (хотя работа отличная проделана), но он обмолвился про прокси паттерн имеет место быть, а дальше написал что получись эффективнее, но по другому


      1. markelov69
        01.02.2023 22:24
        -2

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


        1. kashey
          02.02.2023 00:01
          +5

          Зато без «сюрпризов» - в библиотеках основанных на Proxy каждые пару лет находят по паре багов и их решение часто стоит скорости.


        1. RubaXa
          02.02.2023 09:17
          +3

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

          Если отбросить функции, то кода ровно столько же будет, что на том же mobx, просто без магии Proxy, которые заставляет вас даже примитив засунуть в "объект" чтобы получить геттер, ну а тут вызывать функцию, чем и является геттер ????????‍♂️


          1. markelov69
            02.02.2023 09:51
            +2

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

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

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

            Вот ссылки:
            https://stackblitz.com/edit/react-ts-zri1ry?file=App.tsx
            https://stackblitz.com/edit/react-ts-fp26tt?file=App.tsx,index.tsx

            Если отбросить функции, то кода ровно столько же будет, что на том же mobx, просто без магии Proxy,

            Нет, не столько же кода из-за ручных подписок. Магии Proxy вообще нет никакой, достаточно уделить 15 минут и прочитать про то, как это работает и самому пару примеров написать. Тем более в MobX можно отключить Proxy если вы их так боитесь.
            https://mobx.js.org/configuration.html

            import { configure } from "mobx"
            
            configure({
                useProxies: "never"
            })

            которые заставляет вас даже примитив засунуть в "объект" чтобы получить геттер, ну а тут вызывать функцию, чем и является геттер

            Посмотрите код выше, MobX нужно использовать именно так. В классах.


            1. RubaXa
              02.02.2023 10:31
              +1

              Так у вас там makeAutoObserver, который работает с «объектом» + observer из mobx-react-lite, который является биндингом, а в статье авто же хотел показать, что акту никакие биндинги не нужны, он работает из коробки средствами самой либы.

              Никто не мешал автору сделать свой observer, но опять же цель статьи в другом ????????‍♂️

              Кроме этого, вы говорите что код чище, но внутри у вас магия, нет реактивного примитива, есть контейнер, который при создании автоматически создаёт внутри себя геттеры/сеттеры на описанные свойства без разбору, и так каждый раз.

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

              PS useProxies, я про сам петтерн, а не Proxy как таковой


    1. yogurt1
      02.02.2023 01:25
      +3

      https://stackblitz.com/edit/typescript-decorators-example-ieqdxk?file=index.ts

      Очень хорошо ложиться на ООП с декораторами :-)


  1. Riim
    02.02.2023 09:05
    +3

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

    Один в один повторён пассивный режим вычисления из cellx, тот же глобальный счётчик, та же возможность лишних вычислений при тех же условиях и у меня даже то же самое оправдание для них)). На сколько я помню cellx в этом режиме действительно прилично так быстрее, чем в основном, но у меня этот режим скорее как костыль для автоматической финализации (dispose) ячейки при удалении последнего подписчика, в акте же этот режим удалось сделать основным, что тут сказать, отличная работа проделана!

    UPD: а вообще смотрю я как используются reactions в коде cellx-а и понимаю, что отказаться от получаемого таким образом функционала будет очень больно, функционал тут для меня определённо важнее скорости.


  1. nin-jin
    02.02.2023 12:15
    +5

    Статья уже устарела, расходимся. @krulodреализовал упрощённый $mol_wire, который ещё быстрее и кушает меньше памяти.


    1. artalar Автор
      02.02.2023 18:38

      На разных устройствах результаты разные :)
      https://perf.js.hyoo.ru/#!bench=9h2as6_u0mfnn

      Arc

      Firefox


      1. markelov69
        02.02.2023 19:27

        Че-то с тестами то мухлеж какой-то, я решил проверить на вшивость и вот результаты:
        https://perf.js.hyoo.ru/#!bench=nhg1tu_5gl6nt

        Тут просто разгром в скорости в пользу MobX в разы. ВАЖНО: Консоль должна быть открыта во время тестов! Иначе по причинам известным только одному создателю теста цифры не соответствуют действительности от слова совсем, это даже видно просто по времени выполнения.

        Как проверить на вшивость? Да просто добавить console.log в Setup и далее даже просто наблюдая за скоростью вывода лога в консоль сразу видно что вывод act в разы медлее чем вывод MobX'a.

        1)

        2)

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


        1. artalar Автор
          02.02.2023 20:11

          Очень странное влияние открытой консоли. Бенч не мой, сказать ничего не могу. Вот мой бенч, там мобыкс достаточно сильно сливает, что там не так https://github.com/artalar/reactive-computed-bench ??


          1. markelov69
            02.02.2023 20:45

            У Карловского странное всё, даже бенчмарки) Им вообще верить нельзя, надо всё самому проверять. Я набросал свой, но прям супер простой кейс, добавил сюда ещё CellX и да, ваш вариант быстрее конечно.
            Вот ссылка - https://stackblitz.com/edit/react-ts-1wcwyz?file=App.tsx,index.tsx

            Но всё же на 100тыс синхронных итерациях на клиентской стороне вообще без разницы 20ms или 5ms )) Учитывая то, что сам по себе код с использованием MobX более чистый и нативный, я ему прощаю проигрыш в эти несколько миллисекунд)


        1. nin-jin
          02.02.2023 20:44

          ВАЖНОКонсоль должна быть открыта во время тестов!

          Вы всех своих пользователей заставляете открывать отладчик и ставить там логпоинты с выводом в консоль бессмысленных цифр? Или думаете вывод в консоль ничего не стоит?

          цифры не соответствуют действительности от слова совсем, это даже видно просто по времени выполнения.

          Время выполнения замера зависит от многих факторов, но слабо зависит от скорости работы конкретного кейса. Сетап отрабатывает один раз на замер. Число итераций в замере подбирается таким образом, чтобы замер был не слишком коротким, но и не слишком длинным.


        1. PavelZubkov
          02.02.2023 20:53

          ВАЖНОКонсоль должна быть открыта во время тестов! Иначе по причинам известным только одному создателю теста цифры не соответствуют действительности от слова совсем, это даже видно просто по времени выполнения.

          А есть аргументы в пользу того, что при открытой консоли цифры не соответствуют действительности, а не при закрытой, или в пользу того что они в обоих случаях не соответствуют дествительности?

          С вставка console.log и замер скорости вывода на глаз - слабый и странный аргумент, т.к. глаз не славится точностью, а один прогон занимает миллисекунды. К тому же не ясно, как коррелирует количество запускав одного прогона и кода целиком вместе с импортом(который ассинхронный)


        1. krulod
          02.02.2023 20:57
          +1

          На результат влияет миллион разных факторов. Есть претензии к платформе бенчмарков? К реализации библиотек? Или, может, к самим тестам? Дерзай, нам самим интересно, как открытая консоль повлияла на показатели. А если не разбираешься - не лезь.


          1. nin-jin
            02.02.2023 21:08

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


            1. markelov69
              03.02.2023 07:47

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

              Так почему MobX в таком случае начинает работать быстрее, а cellX и arc медленнее? Если с закрытой консолью они были быстрее или примерно на ровне с MobX в ваших тестах. Логика тут напрочь отсутствует. Если консоль замедляет, то она замедляет всех, а так что кого-то замедляет в разы, а кого-то ускоряет на 40% это просто ерись. Это анти инженерный подход, когда погода в Австралии влияет на результаты измерений уровня радиации в разы, это значит что вы не умеете измерять радиацию. Вот банальный тест и он показывает стабильную разницу в скорости вне зависимости от того, открыта консоль или нет. https://stackblitz.com/edit/react-ts-1wcwyz?file=App.tsx,index.tsx


              1. nin-jin
                03.02.2023 11:48
                +1

                Что творится на вашей машине мне не ведомо, но на моей MobX колеблется возле 120 микросекунд в обоих случаях, а act проседает с 80 до 140. Стоит иметь также ввиду погрешность в 10-20 микросекунд.

                Ну а ваш бенчмарк стабильно сравнивает тёплое с мягким. Для MobX у вас замеряется 1 реакция на 100к изменений одной и той же переменной. Для Act - 0 реакций. А для CellX - 1 реакция на каждое изменение. Вам бы сперва теорию подучить, прежде чем бросаться громкими заявлениями.

                Ну и откройте уже для себя performance.now() или хотя бы Date.now().


                1. markelov69
                  03.02.2023 12:28

                  Что творится на вашей машине мне не ведомо, но на моей MobX колеблется возле 120 микросекунд в обоих случаях, а act проседает с 80 до 140. Стоит иметь также ввиду погрешность в 10-20 микросекунд.

                  Обычная машина, проц Ryzen 9 5950x вот попробовал в Mozilla Firefox

                  Такая же фигня, адское проседание arc почему-то, а MobX гораздо быстрее.

                  Для MobX у вас замеряется 1 реакция на 100к изменений одной и той же переменной. Для Act - 0 реакций. А для CellX - 1 реакция на каждое изменение. Вам бы сперва теорию подучить, прежде чем бросаться громкими заявлениями.

                  1) Как бы в Act идет отложенное выполнение реакций(автобатчинг)
                  2) В MobX runInAction батчит
                  3) В CellX вообще синхронно реакции отрабатывают без батчинга и их как раз 200к и при этом он самый быстрый.

                  Ну а ваш бенчмарк стабильно сравнивает тёплое с мягким

                  Нет, он сравнивает как раз таки в лоб, сколько стоит 2 раза по 100к заинкрементить реактивную переменную, насколько быстро при этом все работает под капотом. А то что Act и MobX батчат изменения, а CellX вызывает реакции синхронно, это уже особенности дизайна данных библиотек, но при этом CellX быстрее всех хоть и 200к реакций заодно выдал, в то время как act и mobx 1 и 2. Но это не суть важно, важно то, что открытая консоль на это НЕ ВЛИЯЕТ НИКАК в отличии от вашей поделки непонятной, где просто тот же Act в 10 раз медленнее начинает работать, а MobX на 40% быстрее.

                  А если брать цифры, то в реальной жизни 100к итераций на постоянной основе приложения не делаю в 99.99999% случаев, и даже если и сделают, то это <30ms, ну на доисторических устройствах пусть 200ms, но это опять же не реальный кейс тем более на постоянной основе, отсюда не важно сколько наносекунд будет сэкономлено в реальной эксплуатации и сколько килобайт памяти сэкономлено, это просто сущие копейки, даже для мобильных устройство в которых самое дно устройство имеет 2гб оперативы.


                  1. nin-jin
                    03.02.2023 15:12
                    +1

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

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

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

                    Наконец, даже на самом донном устройстве, системные процессы занимают от 1 гб. А ваш сайт - не единственное запущенное на нём приложение. И чем активнее он использует память, тем медленнее он работает. Вплоть до того, что один только цикл сборки мусора уже может не уложиться во фрейм анимации. На своём микробенчмарке вы всё это, конечно, же не заметите. А заметите в проде, когда будет уже слишком поздно что-то менять.

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


                    1. markelov69
                      03.02.2023 16:24

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

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

                      чтобы над вами не смеялись более прошаренные коллеги.

                      Более прошаренные? Это кто? Те, которые говорят не разбираешься - не лезь, просто закрой консоль и всё и т.п., они прошаренные да? :D:D:D

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

                      1) Претензии есть и они вполне конкретные и обоснованные, они для вас не удобные, потому что вы не понимаете почему натворили такую дичь, вот и пытаетесь их игнорировать и отбрыкиваться всеми способами.
                      2) Править такой код и разбираться в проблемах его кривой работоспособности - нет, спасибо, кровь из глаз будет мешать печатать.
                      3) Продвинутого? Это который не удовлетворяет самое фундаментальное требование к бенчмарку - повторяемость результатов(хотя бы в небольшом разбеге погрешности) в не зависимости от погоды в Австралии и открыта ли консоль в браузере. Но нет же, у вас просто в разы и десятки раз разнятся результаты из-за вещи, которая на это не влияет - консоль. Отсюда вывод, то что у вас там твориться, это просто неведомая дичь которая не несет ничего общего с реальным тестом тех или иных библиотек.


                      Вот так и быть ваш бенчмарк MobX vs Act по скорости (спойлер разница слабая), прям код скопировал у вас, только в нативном исполнении и где консоль не влияет на результат:
                      https://codesandbox.io/s/angry-currying-5ehe0v?file=/src/index.js
                      https://5ehe0v.csb.app/


                      1. nin-jin
                        03.02.2023 17:55

                        Ну а отсутствие мозга очистки массива между итерациями и даже кейсами - это пять.


                      1. markelov69
                        03.02.2023 18:45

                        Ну а отсутствие мозга очистки массива между итерациями и даже кейсами - это пять

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

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


          1. markelov69
            02.02.2023 22:12

            На результат влияет миллион разных факторов.

            https://stackblitz.com/edit/react-ts-1wcwyz?file=App.tsx,index.tsx
            В результатах нет разницы открыта консоль или нет. О чем это говорит?

            Есть претензии к платформе бенчмарков?

            Да, т.к. результаты плавающие и здравого смысла в них нет. Т.к. открытая консоль: 1) Некоторые результаты делает очень медленные
            2) Некоторые результаты наоборот делает ощутимо быстрее (например MobX и не только)

            А если не разбираешься - не лезь.

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


  1. SWATOPLUS
    02.02.2023 14:42
    +1

    Всем привет, меня зовут Артём Арутюнян

    Я почему-то ожидал здесь увидеть "Дмитрий Карловский"


    1. vanxant
      02.02.2023 17:36
      +1

      вы промахнулись на 1 уровень комментов


  1. andres_kovalev
    04.02.2023 00:31

    Спасибо за интересный материал. Вопрос к автору библиотеки - поддерживает ли она в каком-либо виде асинхронность?