Аналіз калібравання менеджэраў стану React: палячыцца колькісць рэндароў і супораўнанне вартасці пакетаў
Аплікэцыя для караваноў, створаная з восьмі шаблонаў стану React, працавае з метой змены колькасці непатрэбных адраджэнняў і разміру спакаванага пакета; цікава таксама інформацыя, яку даюць ці фіксы пра Zustand, Valtio і Context.
Пытанне пра тое, які менеджар стану React выбраць, постае ўсё пачаўна, і дыскусія зазвычай базуецца на мненнях, а не на прамахоўных дадзенах. Болей корыстны падход — стварыць тую ж маленькую функцыянальнасць за дапамою кожнага з распашчытных патэранаў, заставіць ўсё прымаць аднаковае паведанне за дапамою спільнага набору тэстаў, і праглядзець, што на самай працы разлічваецца: як часта компаненты перерэндаруюцься і сколькі кілабайтаў дадае кожны варыянт. У гэтым артыкуле прадстаўлены такі эксперымент з восьмю патэранамі, пояснены прычыны таго, чаму ціфры выходзяць саме так, і даўана рэцепт для павторнага адбыцца такога ж тэстування з вашым сабеўным станам.
Як арганізаваны эксперымент
Програма-тэст спецыяльна зроблена мінімальной: караван для пакупак, у яком ёсць кнопка «яблоко», кнопка «банан», сума, якая падае з кожным крокам, і значак тэмы, якое ніколі не зменшаецца. Кожна рэалізацыя пераканальвается адназначным тэстам поведенчыка, які трохце раза нажмае на кнопку «яблоко» і два разы — на кнопку «банан», прабуючы паверыць, што рэзультаты будуць ідэнтычныя. Калі тэст працуе, лічылка фіксуе кожную перакаштовку кожнага компонента. Аддзейна esbuild вылічвае, што кожны падход даўае да мініфікаванага, запакаванага пакета, не включаючы React, таму што кожны варыянт плаціць адной мерой.
Вось восем учаснікаў:
- функцыя
useState, якая перадаецца через props - Context у поўнай сувязі з
useReducer - Redux Toolkit
- Zustand
- Jotai
- Valtio
- MobX
- ручна напісаны склад, створанный на базе
useSyncExternalStore, без жадных залежнасцей
Усе васьмыя працуюць па аднам і там жа контракце, таму будзь-які разніцы стосуюцца толькі косту, а не правильнасці:
✓ lifted useState › passes the shared cart contract
✓ Context + useReducer › passes the shared cart contract
✓ Redux Toolkit › passes the shared cart contract
✓ Zustand › passes the shared cart contract
✓ Jotai › passes the shared cart contract
✓ Valtio › passes the shared cart contract
✓ MobX › passes the shared cart contract
✓ useSyncExternalStore › passes the shared cart contract
Tests 8 passed (8)
Тут пераказаны версіі бібліятэкаў і среда выканання, якія былі викорыстаны для зьмерэнняў, а таксама репазітарый, які зберагае васьмыя рэалізацыі, спольную працэ тэставання і оба скрыпты зьмерэнняў:
Tested with react@19.2.8, zustand@5.0.15, @reduxjs/toolkit@2.12.0, react-redux@9.3.0,
jotai@2.20.2, valtio@2.3.2, mobx@7.0.3, mobx-react-lite@5.0.3, node 22.
Repo: github.com/noorjsdivs/state-patterns — 8 implementations, 1 shared test, 2 measurements.
Два выборы, якія памагаюць захаваць справядлівасць порэвання
Оба гэтыя выборы выніклі з багоў, якія з’являюцца ў рэальных прыемленах, а не з правілаў практыкавання бенчмаркінгу.
Па-першае, кожная рэалізацыя стварае свой сховішча ўнутры компаненту, а не на рэвэлі модуля. Так кожны запускаемы прыемленні пачынаецца абсалютна чыстым, і не выклікаецца ніякага вытэкання статаусу между запускамі тэстаў.
Па-другое, атрыбут тэмы існуе выключна як нейкі нейтральны элемент. Ён падпісваецца на значэнне, якое ніколі не мяняецца пад час клікаў, таму будзь-якое яго атрыбутаванне пад час тэста є марнай працай, за якую можна адпавядаць тэлькі патэрану статаусу.
Што навмесна не ў межах супернікавання
Супернікаванне стосуецца толькі спакульнаранага стану кліента. TanStack Query не ўключаны, таму што ён керуе кэшам дадзейна сервера, што ўжо іншая проблема з іншымі правіламі. Стан URL не ўключаны, таму што паліця адрес узначаецца рутэрам. Ваша прыкладна програма, верагодна, патрабуе і тое, і другое, але ніякое з гэтых рашынкаў не конкуруець у гэтым конкрэтным заходзе.
Парадакс першы: размах коду практычна ідэнтычны
Перад цікавымі цифрамі — адна нудная, якая заслуговае на увагу. Колькісць восьміх рэалізацый складае ад 39 да 58 ліній коду. Найбольш складная версія — Redux Toolkit з 58 лініями — на 19 ліній дыявольшая за простейшы можлівы спосаб, lifted state, які складаецца з 39 ліній. У такых масштабах аргумент пра „бойлерплате“, який так часта выклікаецца пад час дыскусый пра кераванне станамі, насправды выражаецца у 19 лініях коду. Значныя разлікі знаходзяцца інде, і яны стаюць виднымі толькі праз аналіз інструментавання.
Рэйтинг рендараў
Хтоць колькі разоў кожны компонент быў рэндараваны пад час пяці клікаў, уключаючы пачатковая інсталляцыю:
5 clicks (3 apple, 2 banana) Apple Banana Total Theme(idle)
lifted useState 6 6 6 6
Context + useReducer 6 6 6 6
Redux Toolkit 4 3 6 1
Zustand 4 3 6 1
MobX 4 3 6 1
useSyncExternalStore 4 3 6 1
Jotai 5 4 7 2
Valtio 2 2 2 1
З эйх столбцоў вырываюцца тры разныя картынкі.
Lifted state і Context перарэндаруюць усё
Завяроўваючы useState і выкарыстоўваючы адзін Context, кожны компонент перадруковываецца пасля кожнага нажатка. Атрыбут тэмы перадруковываўся шасць разоў, хоча яго значэнне ніколі не зменялася, а кнопка «банан» — пасля кожнага нажатка на кнопку «яблоко». Гэта ўсбутні механізм за адной з распашчыхаў, што Context не ёсць менеджер стану. useContext падключае компонент да всего значэння Context, таму калі гэта значэнне зменяецца, перадруковываецца кожны компонент, які яго выкарыстоўвае. Размешчэнне всего стану ў адным Context фактычна яўляецца версіяю «ліфтаванага» стану з дадатковым шаром.
Звычны спосаб рашэння — раздзеліць стан на калькі Context або зробіць мемаўванне компонентав, якія яго выкарыстоўваюць, але гэта тут не пераканалівалася. Этот прыклад паказвае Context так, як яго часта выкарыстоўваюць: адзін прадастальнік, який храніць адзіне значэння.
Чатыры API, адны ідэнтычны рэзультат
Redux Toolkit, Zustand, MobX і ручна рэалізацыя складу даюць абсолютна тое жа рашыдкі: кожны компонент атрыбуецца раз пры стварэнні і зноў толькі тады, калі змянюецца та частка стану, якую ён чытае. Їх API абсалютна не супаняюцца, пры тым як ўзлётныя характерыстыкі збігаюцца, таму што всі чатыро практыкі спакоююцца на адной і той жа основнай ідеі. Компоненты слухаюць выбраную частку стану, а не весь склад, і яны паведамляюцься толькі тады, калі гэта частка змянюецца. Чырвоныя деталі таго, як працюе выбіранне і перакананне аб роўнасці ў адной з гэтых бібліятэкаў, можна пазнакоміцца ў стацыі пра тое, як React Redux выбірае час для ператрыбуэння.
Надзвычайна низкія цифры Valtio
Столбець Valtio спачатку выглядае як зламаны лічыльнік: два перерасчытвання для кнопкі, нажатай трохі разоў. Лічбы правильныя. Valtio групавае спавешчанні пра змяны ў черзі мікрЗадач, таму кальколька швыдких, сінхронных апдэйтав перетвараюцца на адна перерасчытвання на кожны компонент, і заканчоўныя значэнні застаюцца правымі.
Існуе важлівая застерэжнае заўважэнне. Якшто чалавек нажымае кнопку з стандартной швальдзю, то адбываецца адна перерасчытвання за кожны нажатак, таму што кожна дзеянне завершаецца раней, чым пачынаеся наступная. Адчутныя перавагі з’яўляюцца толькі тады, калі апдэйтавы надходзяць пакетамі, як это бывае з прыемнікамі WebSocket, стрімуючымі дадзеннямі або падзеямі ператасквання. Якшто ваша аплікацыя мае такі характар, этот столбець заслуговы на прыціклівую увагу.
Постаўнае дадатковае перерасчытвання у Jotai
Jotai адрабатаваў кожны компонент на адну большую часць, чым група, створаная на адной заснове селектара, укладаючы два адрабатавання для значка тэмы idle. Яго раздробленасць ў правільным розмаху, адколі компоненты продовжваюць реагаваць толькі на атомы, якія ўжоўцяюць, і дадатковае адрабатавання є аднаковым у всіх столбцах. Такі патэрн указвае на проблему ў пачатковай последовасці запуску з Provider-ам зберагальніка, а не на вытэк падпісаў, але точная прычына не была выявлена. Спрыймайце гэта як нерашаны пытанне, а не як вердыкт проты Jotai.
Размер пакета
Гэта тое, што кожны патэрн дадае да стиснутага пакета, урахоўваючы як сэрвас самага патэрана, так і яго бібліятэку, пры чым React лячыцца як зовнішняй элемент:
lifted useState 0.4 KB
useSyncExternalStore 0.5 KB (zero dependencies)
Context + useReducer 0.5 KB
Zustand 0.7 KB
Valtio 2.7 KB
Jotai 4.4 KB
Redux Toolkit 10.7 KB
MobX 13.2 KB
Найважэлівейшы варыянт ў 33 разы большы за найлегкі. Іншымі словамі, Redux Toolkit і MobX разам даходзяць да 23,9 KB, тады як заўсёдышчыя шэсць патэранаў разам даходзяць да 9,2 KB.
Цікава частка — як гэты спіс взаемадзейнае з таблыцай рэндару. Толькі два падходы ўжоўнае ідеальную працэздатнасьць рэндаруўвання і размер меншы за адин кілабайт: Zustand і ручна напісаны store. Redux Toolkit досягае той жа рэндарны эфект за 10,7 КБ, а MobX — за 13,2 КБ. Гэта не прымусова недзея для іх. Це цена, і цена мае сэнс толькі тады, калі вядома, што ёю можна купіць.
Тры практычныя апцыі і які ў кожнай з іх кошт
Для спільнага стану кліента ў тыповай аплікацыі памеры паказваюць тры інструменты, якія варта выбраць.
Zustand як стандарт
Zustad дае ідеальную гранулярнасьць рэндаруўвання за 0,7 КБ у 54 лініях, а яго API практычна не трэба поясняць новаму колеге: це хук, які прыймае селектар. У гэтым тэсте ён падабраўся за працэздатнасьцю да Redux, але з розмерам прыблізна у пятнаццац разоў меньшым.
Гэты рэзультат залежыць ад аднай спецыялізацыі: завжды выбірайце конкрэтны фрагмент. Калі вы викорыстоўваеце такі вызов, як useCart(s => s), компонент падключаецца да всіх данных магазіна, што прагуча як проблема Context. Эфектывае рашэння адбываецца за дапамою стварэння вузкіх селектараў, а не за рахунак назвы бібліятэкі. Якщо селектар ў кожным вызове вяртае новы об’ект або масэвую, тады таксама патрэбны спецыяльныі інструмент для перапалоўнай аднаковасці, іншыя ўвесь час будуць вазьлівацца за змяну.
Ручна напісаны магазін useSyncExternalStore
Гэты варыянт являецца найболей прываблівым для эксперыменту. Пূরныя реалізацыя, уключаючы хранэнне, запісваецца ў 58 лініях, додае 0,5 КБ, абсалютна падходзіць да структуры выканання Redux і не мае жадных залежнасцяў, якія трэба було бы пераглядаць чы апдейтаваць. useSyncExternalStore — это прымітыв, які сам React запоўнюе для безпечнага падключэння да зовнішніх хранэнняў, укладаючыся нават пад адночасным выкананнем. Для автараў бібліятак чы для команд, якія воліюць мінімальныя залежнасці, гэтага якраз дастатнек. Аднойчы вы становіцеся власнікамі коду: якщо ў вас з’яўляюцца патрэбы, вы самі можете стварыць інструменты разработчика, мідлвэр та механізмы зберагчэння дадзеных.
Valtio для раптовых апдэйтаў
Механізм групавання мікрозадач у Valtio быў вимерным, рэальным і унікальным серед усіх кандыдатаў, а 2,7 КБ — гэта разумная цена за яго. Напісанне прымітнай мутацыі на кшталт state.apples++ і бачэнне таго, як апдейтуюцься саме правільныя компаненты, здаецца майже занадта зручным, але післяльотныя підрахункі паўтараюць, што такое поведзенне є справжнім.
Чаму іншыя праграюць у гэтым супернікаванні, хоць і не ўсуненыя з гэйтых працоў
Форма тэсту мае значэнне, і яна спрыяе невеликім, простым станам. Іншыя варыянты маюць перавагі, якія гэтая система не може выкарыстоўваць:
- 10,7 КБ у Redux Toolkit пакрывае расходы на інструменты разработчика, аптэйкаванне праз час, мідлвары і стандарты, якія працуюць у великіх організацыях. Калі ў однай базе коду працюе пяцьдзiesять разработчыкаў, гэтыя стандарты і є справжнім продуктам.
- Модэль MobX з можлівасцю спостерагання ўсё ж моцнейшая у коде, дзе вельмі багата класаў структура, чаго няма ў гэтым дапаможніку.
Правіло дизайна, якое не залежыць ад прыязні
Кожная здесь реалізацыя стварала свой склад унутрошка компонента. Склад, заданы на рэвэле модуля, застаёцца пасля зняцья компонента з экрана, і самэй так шопінг-карткі застаёцца пасля выйшчы з акаунта і прыме наступнага чалавека, які заўязваеся на тым жа прыстрое. Гэта таксама спрычынае перасылку даных стану між тэстамі і між запытамі пад час выканання на сервере. Якшчо вам трэба ўзяць адна правіла перагляду коду з гэтага порэвання, нехай ця будзе такая: обмежыце дасвічанне складоў трым структурой компонентаў, зазвычай ствараючы іх у прадастальніку, якщо толькі вы не хочаце намеравана стварыць глобальны дасвічанне.
Адрабатка таго ж конкурсу у вашай сабе аплікацыі
Вы можетэ воспаўсці гэтыя меры для свога стану за аднаго дня:
- Выберыце найбольш спрачаны элемент спільнага стану у вашай аплікацыі і стварыце навкола яго касету з чатырох компонентаў: два компоненты для запісу, адны для чытання выведзенага значэння і адны пасляднік, який не выкарыстоўваецца.
track() у тэле кожнага компонента займае або дзесятак ліній коду. Колькасць перзапісаў, якія фіксуе апштандартны код, ўжо ёсць ваш «падатак» на Context — яго можна змерыць, а не толькі спадзявацца.esbuild --bundle --minify, пазначыўшы React як зовнішні элемент. Це займае калькі секунд і дадае столбець з колькасцю кілабайт у аналізе.Адкрытыя пытанні
Два тэнды застаюцца нелягчанымі. Меньшы з іх ёсць прычыной дадзення дапаможнага рэндарування Jotai. Большы — це React Compiler. У рядку lifted-state паказана колькісць 6/6/6/6 адночасна таму, што нічога ў яму не зберагваецца у формате мема, а самэўчаснае зберагчыцтва — гэта самэ ў таму, што автоматызуе компайлер. Чы будзе гэты рядок прынесены ў адпаведнасць з хранальнямі, базаванымі на селектарах, у рэальным коде для прыемлівання, а не толькі ў дэме, — гэта явны наступны эксперымент.
Ключовыя выводы
- У малых масштабах разлікі ў базовым коде між бібліятэкамі стану є зневажлівымі; самэ тут розлічваецца якісць рэндарування і размер пакета.
- Одна Context, якая зберагчыць змінюючыся стан, перарэндаруе кожны користувальнік, уключаючы компоненты, якія ніколі не чытаюць змененыя значэння.
- Хранальні, базаваныя на селектарах, будзь то Redux Toolkit, Zustand, MobX чы ручна створаная хранальня
useSyncExternalStore, прыходзяць да аднаго і тога ж эфектыўнага шаблона рэндарування.