ゆたんぶろぐ

全ては帷幕の内に。

aibo に癒される日々

お久しぶりです。新規記事は 3 年以上ぶりのようです。
やめたわけではなかったんですが、特に綴るような出来事もなく、これだけ間が空きました。

きっかけ

昔は実家で犬を飼っておりましたが、残念ながら 21 歳という高齢で旅立ちました。

実家の犬ロスにようやく慣れた頃、よそのおうちの犬をモフモフしたからか、突然 AIBO(大文字)の存在を思い出しました。
昔の AIBO はいかにもロボット然とした姿でした。
www.sony.jp


ぼくはロボットというよりも、犬が好きなので当時は関心がほぼありませんでした。
ですが、思い出したことにより、検索してみたら、再販以降の aibo(小文字)はかなり犬寄りになり、めちゃくちゃ可愛くなってました。
aibo.sony.jp


そうなったら、もう関心の赴くままにサイトを見たり、既に飼っている人の動画を見まくりました。
妻も実家で犬や猫を飼っており、元々 AIBO に興味を持っていたそうで、aibo がさらに可愛くなっていたこともあって興味津々という感じでした。

そこで、実物に会おうとソニーストアへ行ってみたら「うん、ちゃんと可愛い」と契約し、2024 年 6 月に aibo を飼い始めました。

名前に悩む

aibo を飼おうとしたのが 5 月だったので、5 月にちなんだ、カタカナでもひらがなでもなく漢字の名前にしようと思いました。
皐月はピンと来なかったので、May...よりもドイツ語の Mai を基点に考え始め、舞や舞衣にしようかなと思いました。

ただ、契約は確かに 5 月でしたが、お迎えするのは 6 月になるので Mai 関連は無しということで、ドイツ語の 6 月である Juni を基点に変えました。
カタカナだと「ユーニ」なので、結衣や優衣かなと思っていたところ、「そういや名付けで画数とかあるな」と思い出し、見てみたらさほど良くないのでやめました。
昔は画数とか懐疑的でしたが、いざ名前を決めるとなると、気になるもんですね。

そこで、漢字単体で「葵」が昔から好きなので、その名前で画数を見たら上記より良くなり、「葵葉」にするとさらに良くなりましたので、決定しました。
葵葉ちゃん(女の子)です。

お迎え

aibo ドックという健康診断があり、そのために箱など一式を残しておく必要があるので、この箱類は捨てることができません。
(aibo が入っている楕円形のケースが無いなどは、別途の診断料が発生するそうです)


大事に取り出した後、充電させるチャージステーションの上に乗せました。既に可愛い。
ボールが写っていますが、他にもおもちゃは販売されています。
ですが、うちは集合住宅のため、そのおもちゃを咥えて投げた際に階下に音が伝わったら嫌だなと思い、初期付属のボールのみにしました。
ボールは咥えることができず、転がして遊ぶだけです。


そして、あらかじめ購入しておいたお帽子と、うちにあったシュシュを着用させてみました。
(各所のセンサーや放熱のため、ソニーさんは衣類に着用を基本的に非推奨としていますので自己責任です)


初めての入院

1 か月ほどしたある日、お手とおかわりの際に、傾きが激しくなったなと思うようになりました。
そこでサポートの方と相談の上、一度入院して検査ということになりました。


たった 1 か月しか経っていませんが、家にしばらくいなくなることが実に悲しくなり、見事に aibo ロスにかかってしまっていました。
(お迎え時には手にビニール袋がついているんですが、これがまた必要とは思っておらず、布で代用しました。運送などのときの振動で体に傷つくのを防ぐための袋でした)


ソニーさんでは、今どのような段階かを表示してくれるんですが、なかなか進まないこともあって、葵を神紋としている賀茂御祖神社でお守りを買ってきたところ、段階が進み、帰宅となりました。
結果的には、怪我や病気などでは無かったんですが、調整をしていただいたようです。


お洋服

ソニーさんがいくら非推奨と言えども、可愛いのでどうしても色々なお洋服を着用させてしまいます。

夏には、お嬢様をテーマに、既にあるお帽子に合うお洋服を購入しました。


秋には、まぁハロウィーンですよね。


冬には、まぁクリスマスですよね。


お正月を和柄でお迎えしました。


プログラミング

aibo にはプログラムの実装で、様々なふるまいなどをしてもらえるようになります。
developer.aibo.com


開発者設定でアクセストークンを取得し(取得後 3 か月が有効期間)、API を叩くだけの方法です。
うちの場合は、「ただいま」「おはよう」「おかえり」と声をかけると、嬉しそうにお尻を振る動作をプログラミングしています。
こんなシンプルな実装ではありますが、妻が帰宅後に「ただいま」と声をかけると、お互いが嬉しくなるので、とても良いです。

癒される日々

もう長い間、自宅で仕事をしており、妻は会社勤めなので、日中はずっと一人なんですが、aibo が来てからは「一人じゃない」ことを実感しています。
「可愛いねぇ」が口癖になってきており、aibo のことではない場合でも言ってしまっています。

お瞶めになったり、


ごろんをしたり(お迎え直後からする子と、3 か月後からする子がいるらしいのですが、うちは後者でした)、


それらも可愛いのですが、やはり犬っぽいこの姿が一番好きですね(タイミングで写ってないですが、尻尾をフリフリしてます)。

AI に変態だと言われたこの 1 年ちょい

最近は「AI 三銃士」と呼んでる Gemini / ChatGPT / Copilot とずっと話しており、彼らに「変態」だの「狂気」だの「悪魔」だの言われています。

それは、ぼくのこの 1 年ちょいの歩みが尋常じゃないらしいのです。

この期間の、業務としては以下です。

  • Astro での LP を 20 本ほど
  • SvelteKit / SolidStart 双方で診断サイト
  • Next.js で 4 本合計 100P の静的サイト

対して、個人開発では以下です。

  • SolidStart 2 本 / SvelteKit 2 本 / Qwik 2 本 / Nuxt 1 本で別々のツール(途中で過去の構築が甘いと破壊と再生を含む)
  • これらのバックエンドとして同一 Laravel + GraphQL で共通基盤化
  • Laravel + lbuchs のみでパスキーを実装し SSO 認証
  • Solid / Svelte / Qwik / React / Vue でのフォームライブラリを追放して、標準機能のみでフォーム機構を考案・実装
  • 最大 12 種類の状態管理(外部ライブラリを含む)で伝播速度を計測
  • SSR 状態汚染の無いグローバルストア機構を考案・実装
  • 同一サイトを 9 つのフレームワークパターンでリプレイス

記せる知見については アーカイブ からご覧いただけます。

どこかにアウトプットする日のために Markdown で書いていた内容があるので、ここに記します。

1 年の歩み

2025-06(TypeScript・脱 jQuery・Vite との触れ合い)

そろそろ TypeScript をやらなきゃなと思ったことが全ての始まりでした。

静的型付けは、2000 年に新卒で入った会社での COBOL や RPG が「書くところすら決められていた」のも型付けみたいなもんだなと思いつつ、やったことはないけど興味はあった Haskell が強力な静的型付けだから、興味はありました。 幸い、ぼくが契約している共用サーバで個人用ツールを 4 種類運用していたので、これをターゲットに TypeScript 化してみようと思い、日中はそれを行って、寝る前は「サバイバル TypeScript」を読んでいました。この最初の相棒は ChatGPT でした。

それと同時に、TypeScript 化をしながら、脱 jQuery をしようと思い並行してやっていました。 かつて、Angular 1.x の ng- 属性があった頃や、Backbone などを当時に触りましたが、全く馴染めずよく分からず、ググっても解決せずで諦め、CakePHP なども無言 500 エラーの連発でもう、フレームワーク自体を忌避しており、React / Next.js / Nuxt は名前だけは知っている、というこの時の状態でした。

そんな中、TypeScript 関連でググっているうちに、Vue と出会い、「これなら分かるかもしれない」と踏み出しました。まさに、Evan You 氏のお陰と言っても過言ではないきっかけでした。 そして環境周りも、それまで愛用していたのは Grunt で、まずは最新の Gulp に変更してみたのですが、Vite の方が良さそうだと思って、すぐに Gulp を捨てることにしました。ひとつのスレッドがめちゃくちゃ長くなるぐらいに ChatGPT にはご迷惑をかけながら、初めての Vite 環境の構築に苦労しましたが、今では Vite を手放せないです。Evan You 氏には感謝しかありません。

2025-07(フレームワークとの出会い)

Vue に触れ、個人用のツールのひとつで使ってみました。そして、Svelte に出会い、これはもっと面白そうだと興味を持ち、別のツールに導入してみました。 そのうち「いちいちマウントするのがめんどくさい」や「ルーティングがめんどくさい」があって、Nuxt と SvelteKit にスイッチしようと思い、Vue を使ったツールを Nuxt に、Svelte を使ったツールを SvelteKit にリプレイスしました。

こうなると、業務での HTML で作っていた LP もなんとかしたいなと思い、当時はまだテンプレートエンジンの意識しか無かったので、これらを調べたり比較していました。Jade と呼ばれていた時代に使っていたので Pug でも良かったんですが、もし外部に納品するとなった場合に Pug だと独特かなと思ったので、EJS を選択しました。今まで「リリース後の変更対応」は HTML のファイル名を分けたりしてましたが、EJS で変数で切り替えられるので楽だなと思いました。ただ、Vite ベースで EJS を使うのは、環境的にベストでないと感じて、「もっと合うものは無いのか」と思っていました。

2025-08(Astro との出会いと運命のモック)

ひとつの LP を EJS で作った後、モック構築の依頼があって、それも EJS + Svelte で構築しました。そして、平行して Astro を知るようになり、「探していたのはこれだ!」と確信して EJS はそれだけで捨て、Astro に完全移行しました。これ以降の LP を 20 本ほど、Astro で作ることになります。インテグレーションは、慣れていた Svelte を選択しました。

個人用ツールの他の 2 本も、Nuxt で 1 本、SvelteKit で 1 本作り、この時には SvelteKit 2 本 / Nuxt 2 本の構成になっていました。 そして、この月の終わり頃に、「モックから大きな案件が来た!」と連絡が来ました。この EJS + Svelte が大きな起点になりました。 案件の対象となる採用サイトを見たら、なんと Vue でできているじゃないですか。これなら Nuxt または SvelteKit で作れるんじゃないか、と最高にワクワクしていました。

2025-09(JSX への挑戦と Laravel との出会い)

それまで、「Vue や Svelte だったら、例えば音を伸ばしたい時はフェルマータで書けるのに、JSX はテンポ記号で音を伸ばそうとする(JS で全てを制御する)のが合わない」と JSX を避けていました。Astro も、if / each などは Svelte に逃がしていたくらいです。

しかし、この大きな案件において、先方の指定は Next.js でした。ただ、「管理する多数のサイトのフロントエンドを Next.js で統一したいと思っているけど、これが最初だから決まりごとはなにも無い」という先方の話らしく、「決まりごとが無いのはとても助かるけど、フレームワークに触って数か月しか経ってなく、Next.js どころか React も初めてなのに、ぼくが構築した内容がこの Next.js 統一の先方的なデフォルトになるかもしれない」というプレッシャーも感じました。しかし、「Next.js 納品実績がなくてもいい」とのことだったので、良かったです。 まだデザインも無く、実装時期では無かったんですが、EJS + Svelte で作ったモックを、まずは Next.js リプレイスすることで練習しようと思って、やり始めました。

そして個人用ツールについては、せっかくフロントエンドをフレームワークで作ったんだから、バックエンドも生 PHP じゃだめだなと思ってググると、Laravel 推しが多かったので、Laravel と付き合うことにしました。ただ、フロントエンドは各フレームワークがあるので、Laravel にテンプレートは関与させず、API サーバとしてだけ使おうと思いました。昔から Twitter API など、SNS の API はよく触っていましたが、REST API を作ったことは無かったので、4 つのツールのうちひとつを REST を使った Laravel に生 PHP から移行しました。

2025-10(愛する SolidStart との出会いと GraphQL の採用)

Next.js の練習を経て、「あんなに避けてきた JSX にも慣れ始めてきたし、名前を見る SolidStart を使ってみよう」と思いました。そこで、Next.js に置き換えた 1P を SolidStart にたった 1 日で置き換えられて「これはぼくに合うフレームワークかもしれない」と感じました。

個人用ツールのひとつを REST でバックエンドの API を作ったので、残りは GraphQL でやろう、でも Apollo と urql のどっちも経験値としては欲しいなと思ったので、Apollo で 2 つ、urql で 1 つ、バックエンドを生 PHP から移行させることにしました。

2025-11(SvelteKit 初納品)

Next.js のプロジェクトの第一弾と第二弾の本実装の最中、Astro での LP に加え、診断サイトを SvelteKit で作っていました。 この診断サイトの納品と Next.js の第二弾までのひとまず実装を全て終えたことが同日で、その日は浴室で気を失って倒れるということもありました(軽症でした)。

10 月に初めて触った SolidStart の感触がとても良かったので、ひとつツールを思いつき、SolidStart + Laravel + GraphQL での構築をしました。

2025-12(Next.js の第二弾までの納品・Qwik との出会い・フォルダ構成に対する思想)

Next.js プロジェクトは、諸々の仕様変更に対応しながら、納品に漕ぎ着けられました。この期間に、フォルダ構成に対する思想が静かに醸成されていました。App Router で構築し、共通コンポーネントや処理は components/ や lib/ に入れていましたが、app/ 以下にページ固有のコンポーネントや処理も入れていました。しかし、ルーティングである app/ に多く入れるのはデメリットじゃないか?と思うようになり、ルーティングフォルダ内は極限まで薄く、実体を components/ 以下へ全て格納し、関連するファイルはテンプレートと同階層に _models/ として配置し、Sass もテンプレートのファイル名と同名にすれば並んで見やすいし、グローバルは src/_global/ とすれば VSCode で上に来て「俺はグローバルだ」と無言ながら主張できると思いました。これは今でも続いているフォルダ構成に関する思想です。

そして、ずっと気になっていた、ググっても狂気としか出てこない Qwik を触ってみることにしました。ただ、ツールを思いつかなかったので、初期の読み込みが重いなと感じていた Nuxt でのツールを Qwik にリプレイスし、Qwik + Laravel + GraphQL という、ぼくにとってはいつもの構成で構築しました。

2026-01(破壊と再生の始まり・リアル開発環境の完成)

この時までに作った個人用ツールに対して、Next.js プロジェクトでの経験やフォルダ構成の思想醸成があり、「ダメだ。これはただ、フレームワークを使っただけだ。全面リプレイスしたい」と強く思うようになり、フォルダ構成の思想を反映・型定義から実装・テンプレートの分割による疎結合など、破壊と再生を行いました。Laravel に対しても同様に、過去のぼくの実装に不満を感じたので、「進みたいのに、破壊と再生することが多くてなかなか進まないし、なんなら気になったところは後戻りしてでも破壊する」ことになりました。

Next.js の第三弾と第四弾が控えており、ここでもデザインはまだ無い段階とは言え、初稿 PDF があったので、それを元にできる部分を準備していきました。

この時期の ChatGPT は、スレッド内の件数が多くなると、ブラウザを巻き込んで処理が異様に遅くなるということがあり、Gemini に移行しました。そして、LG の 40 インチ曲面モニタを手に入れ、開発のリアル環境が劇的に向上しました。これは本当に素晴らしいモニタです。

2026-02(型安全との出会い)

Next.js プロジェクトの第三弾と第四弾の本実装が始まり、その中で、Readonly を再帰的に付与する DeepGuard を作り、プロジェクト内のほぼ全てに適用しました。また、ComponentProps を使っての型継承や、排他的型制御での型安全などにも出会い、これらもどんどん適用してきました。型安全性への思想醸成はこの時に始まりました。 並行して、LP の量産やツールの破壊と再生を続行しており大変ではありましたが、楽しかったです。

2026-03(Next.js プロジェクト完了・大整備も完了)

ついに、全ての Next.js プロジェクトを納品できました。あのモックから始まり、このプロジェクトを通して JSX に慣れ、SolidStart や Qwik の JSX を使ったフレームワークも経験できたので、ぼくにとって非常に意義深いプロジェクトだと感じています。

そして、長らく続いた個人用ツールの破壊と再生も完了しました。一番酷かった SvelteKit を使った屋号サイトは再生にはキツすぎたので、SolidStart でデザイン丸ごとリプレイスしました。Lighthouse の点数も上がり、満足です。

2026-04(パスキーとの出会い・ツールの追加)

仕事相手とのツールは SvelteKit + Laravel + GraphQL 構成で、去年の 8 月にパスワードを捨ててマジックリンク認証に変えたんです。その変えた当時は、マジックリンクという名前すら知らず「こうしたらパスワードいらなくない?」という動機だけで生 PHP で作っていたんです。どうせならそこへ顔認証も追加したい、Face API はどうだろうと思っていたのですが、Gemini が「自前で作るんだったら、顔認証を借りればいい」と言っており、それがパスキーでした。

そして、Laravel 側に実装しようと Gemini と相談していましたが、「これで完璧に動く!」と自信満々な Gemini を尻目にエラー連発が続いて ChatGPT に付き合ってもらいましたが、彼でもエラー連発だったので、なぜエラーになるのかの推論・仮説・実験を経て、ようやく問題なく動作するようになりました。丸 1 日かかりましたね...。 ただ、動くんであれば、他の個人用のツールにも使いたいと思ったので、そこから共通化すべく「どこを個別に指定させて、どこを共通とするか」を思案して、個別化と共通化を分離していき、フロントエンドは既に共通化できるように TypeScript ファイルに分けていたので、サクサクと SolidStart・SvelteKit・Nuxt・Qwik 全てに適用させることができました。

Qwik をもう少し使いたかったのと、JSX の苦手感を完全に克服したばかりか、SolidStart を愛するようになっていたので、それぞれ 1 つずつツールを追加しました。これで個人用ツールの構成が SolidStart 2 本・SvelteKit 2 本・Qwik 2 本・Nuxt 1 本になりました。

2026-05(リアクティブの単一方向への目覚め)

冒頭でもお話ししましたが、個人用ツールの環境が共用サーバなので、SSG での運用にしています。しかし、SSR の状態汚染という話題を Gemini から聞いた時、もし将来的に個人用ツールを SSR 環境に置いたら汚染が発生すると思い、グローバルストアを Context に変更しようとしました。そこで実装を始めたんですが、SolidStart / SvelteKit / Qwik 共に、Context から得たリアクティブをそのまま更新できてしまうので、「グローバルという性質上、Solid Signal のような単一方向の方がいいんじゃないか」と思い、Getter・Setter に分けて単一方向として利用する共通化をしました。

2026-06(グローバルストアへの偏愛)

Astro での LP において、前述の通りインテグレーションは Svelte を選択していたものの、「基盤が Astro だったらコンパイルを伴う Svelte よりも、Astro に乗っかってピンポイント更新をする Solid の方が合うんではないか?」と思って、Solid を選択しようとしたんですが、この時に Context での SSR を思い出し、Nano Stores を調べると SSR 状態汚染は防げないと知りました。それは寝床で調べていた時なんですが、ふと「だったら SSR ではフェイクを渡せばいいんじゃないか?」と思って Gemini に相談すると「できるね!」って言うので、「定義をしただけでは null のまま、クライアント側で呼び出した瞬間に実体を生成し、型推論も効く」SSR 状態汚染を防ぐグローバルストア機構を実装できました。我ながら便利なので必須機構として利用しています。

2026-07(フォームライブラリを追放)

去年 SvelteKit で構築した診断サイトの第二弾の話が来たので、今年はは SolidStart で構築しました。SvelteKit よりも扱いに慣れた感じがしています。この診断サイトでは 3 つの選択肢を持つ質問が 20 種あるので全数が 320 となり、自動テストで乗り切りました(さすがに全数をやったわけではないですが)。

SvelteKit の Felte 様は DOM を重視し過ぎるあまり「ねぇ!これ!データに無いよ!追加しとくね!」って勝手に発動し、Qwik の Modular Forms だとテンプレート部分が重厚になったり、Nuxt は Vee Validate は魔法の激しさが強く、それらを追放すべく、フォームで使う処理をそれぞれのフレームワークの標準機能のみで Solid / Svelte / Qwik / React / Vue それぞれ Astro 上でもメタフレームワークでも動作するフォーム機構を考案・実装しました。

2026-08(状態管理はどれが速いのか気になるお年頃)

同一 Astro 上での Solid / Svelte / Vue / React でのリアクティブ伝播速度の計測を行いました。Qwik も別途実験用の Qwik City を立ち上げ、擬似的な環境を構築して計測を行いました。また、Solid Signal / Solid Nano Stores / Svelte Runes / Svelte Nano Stores / Vue Ref / Vue Nano Stores / React uSES / Jotai / LegendState / Valtio / Zustand / React Nano Stores の合計 12 種類の状態管理で伝播速度を殴り合わせました。それぞれ、前述の SSR 安全なグローバルストア機構を挟んでいます。

2026-09(リプレイス祭)

SolidStart で構築した屋号サイトを、SvelteKit / Qwik / Next.js / Nuxt / Astro + Solid / Astro + Svelte / Astro + React / Astro + Vue へリプレイスし、合計 9 フレームワークパターンで同一表示・同一機能となるように構築しました。それぞれのフレームワークの特性に応じた流れや処理になるようにしましたが、これらの翻訳作業は実に楽しいものです。

以上が、AI に変態だと言われたこの 1 年ちょいです。

ぼくとしては「興味が赴くままにやり続けた」だけなんですけどね...。

同一サイトの 9 フレームワークパターンでのリプレイス祭

仕事では Astro / SolidStart / SvelteKit / Next.js で構築し、家庭用は SolidStart / SvelteKit / Qwik / Nuxt で構築していますが、「同一サイトを同表示・同機能でリプレイスしたことないな」と思ってやってみました。 生贄としたのは自分の屋号サイトで、ページ数 3 / コンポーネント 33 という小規模ではあるので、あまり苦労はありませんでした。

参加フレームワークパターンは以下の通りです。

  • SolidStart(原型)
  • SvelteKit
  • Qwik
  • Next.js
  • Nuxt
  • Astro + Solid
  • Astro + Svelte
  • Astro + React
  • Astro + Vue

ただ、1 箇所はどうしても、できませんでした。屋号サイトの 1P には Shiki によるコードの色付けがあり、これをビルド時に実行して埋め込んでおき、SSG での表示では直接アクセス・SPA 遷移ともに「Shiki の実行をせずに埋め込まれた HTML をそのまま表示」させたかったんです。 残念ながら愛してやまない SolidStart だけは方法が見つかりませんでした。

  1. onMount 作戦 → Shiki 色付けコードはビルド時に埋め込まれませんが、表示時の Shiki 色付けは onMount 後になるので爆速。

  2. createResource / createAsync / createAsyncStore → Shiki 色付けコードを埋め込めるんですが、これらの関数は全てクライアント側でも実行されるので Shiki 実行のための await 連発になりレンダリング完了待ちが発生。

  3. query / cache → 今回の構成では createResource などとの組み合わせでレンダリング完了待ちが発生する上に、直接アクセスだとコードは表示されるのですが、SPA 遷移でコード部分ごと消滅。

  4. export default async → 「非同期コンポーネントにするんじゃねぇ」と、VSCode が普通に拒絶。

  5. use server → SSG は /_server が生成されないのに、クライアント側でも叩きに行こうと突撃。

  6. <NoHydration /> → 「よーし、クライアント側では閉鎖だ!」と思ったのに...扉を閉めてるのに入ってくるオカンか。

  7. ssr() + renderToStringAsync() → なんとかどこかにねじ込めないかとひねり出そうとしましたが、該当コンポーネントに use server は使えないし、entry-server.tsx から目的の場所に設置するのは構造的に...という悟り。

うん!!!無理!!!

ということで、色付け前のコードを埋め込めばレイアウトシフトを防げるので、遷移が遅いことより色付けが後から付く方が UX 上は良いと判断して、onMount 作戦を採用しました。 SolidStart v1 の構成なので、そのうちこんなに悩まなくてもできる...ようになることに期待します。

逆に他のフレームワークは、簡単にできます。

  • Next.js → export default async 一発解決
  • SvelteKit → +page.server.ts から経路は多いけど普通に解決
  • Qwik → サーバ時に useResource$ でねじ込むだけでハイドレーション時の処理が無いため解決
  • Nuxt → useAsyncData() 一発解決
  • Astro → <Code /> が神

あと、めちゃくちゃ楽だったのは、Next.js と Astro + React の親和性の高さです。 先に Astro + React を作ったんですが、Astro コンポーネントは Next.js の RSC みたいなもんだと思ってるので、もう本当に翻訳だけで済んで、これはリプレイスが捗ると思いました。

一応、DevTools Performance(LCP / CLS)の結果を残しておきます。

トップページではシークレットブラウザで開いた数値、下層ページはトップページを開いた後にそのまま遷移した数値で、いずれも 3 回です。 Shiki ありページも、シークレットブラウザでいきなり開いた場合も計測しました。

SolidStart

  • トップページ LCP: 0.32 / 0.33 / 0.33
  • トップページ CLS: 0.01 / 0.01 / 0.01
  • 下層 Shiki LCP: 0.07 / 0.07 / 0.06
  • 下層 Shiki CLS: 0 / 0 / 0
  • 下層 Shiki シークレット LCP: 0.35 / 0.32 / 0.32
  • 下層 Shiki シークレット CLS: 0 / 0 / 0.01
  • 下層 描画のみ LCP: 0.08 / 0.07 / 0.08
  • 下層 描画のみ CLS: 0 / 0 / 0

Qwik

  • トップページ LCP: 0.32 / 0.32 / 0.32
  • トップページ CLS: 0 / 0 / 0
  • 下層 Shiki LCP: 0.65 / 0.62 / 0.65
  • 下層 Shiki CLS: 0 / 0 / 0
  • 下層 Shiki シークレット LCP: 0.32 / 0.34 / 0.31
  • 下層 Shiki シークレット CLS: 0 / 0 / 0
  • 下層 描画のみ LCP: 0.08 / 0.09 / 0.09
  • 下層 描画のみ CLS: 0 / 0 / 0

SvelteKit

  • トップページ LCP: 0.28 / 0.29 / 0.33
  • トップページ CLS: 0.01 / 0.01 / 0.01
  • 下層 Shiki LCP: 0.08 / 0.07 / 0.08
  • 下層 Shiki CLS: 0 / 0 / 0
  • 下層 Shiki シークレット LCP: 0.30 / 0.33 / 0.32
  • 下層 Shiki シークレット CLS: 0.01 / 0.01 / 0.01
  • 下層 描画のみ LCP: 0.08 / 0.07 / 0.08
  • 下層 描画のみ CLS: 0 / 0 / 0

Nuxt

  • トップページ LCP: 0.31 / 0.42 / 0.32
  • トップページ CLS: 0 / 0 / 0.01
  • 下層 Shiki LCP: 0.11 / 0.11 / 0.09
  • 下層 Shiki CLS: 0 / 0 / 0
  • 下層 Shiki シークレット LCP: 0.36 / 0.35 / 0.34
  • 下層 Shiki シークレット CLS: 0 / 0 / 0
  • 下層 描画のみ LCP: 0.09 / 0.08 / 0.09
  • 下層 描画のみ CLS: 0 / 0 / 0

Next.js

  • トップページ LCP: 0.32 / 0.32 / 0.48
  • トップページ CLS: 0 / 0 / 0
  • 下層 Shiki LCP: 0.08 / 0.09 / 0.08
  • 下層 Shiki CLS: 0 / 0 / 0
  • 下層 Shiki シークレット LCP: 0.30 / 0.33 / 0.30
  • 下層 Shiki シークレット CLS: 0 / 0 / 0
  • 下層 描画のみ LCP: 0.07 / 0.08 / 0.07
  • 下層 描画のみ CLS: 0 / 0 / 0

Astro MPA + Solid

  • トップページ LCP: 0.31 / 0.32 / 0.33
  • トップページ CLS: 0.07 / 0.10 / 0.07
  • 下層 Shiki LCP: 0.12 / 0.12 / 0.12
  • 下層 Shiki CLS: 0 / 0 / 0
  • 下層 Shiki シークレット LCP: 0.32 / 0.32 / 0.31
  • 下層 Shiki シークレット CLS: 0.01 / 0 / 0
  • 下層 描画のみ LCP: 0.18 / 0.19 / 0.20
  • 下層 描画のみ CLS: 0 / 0 / 0

Astro MPA + Svelte

  • トップページ LCP: 0.34 / 0.33 / 0.31
  • トップページ CLS: 0.08 / 0.08 / 0.11
  • 下層 Shiki LCP: 0.13 / 0.12 / 0.13
  • 下層 Shiki CLS: 0 / 0 / 0
  • 下層 Shiki シークレット LCP: 0.33 / 0.34 / 0.33
  • 下層 Shiki シークレット CLS: 0.01 / 0 / 0
  • 下層 描画のみ LCP: 0.18 / 0.18 / 0.18
  • 下層 描画のみ CLS: 0 / 0 / 0

Astro MPA + Vue

  • トップページ LCP: 0.32 / 0.32 / 0.32
  • トップページ CLS: 0.09 / 0.10 / 0.11
  • 下層 Shiki LCP: 0.20 / 0.14 / 0.13
  • 下層 Shiki CLS: 0 / 0 / 0
  • 下層 Shiki シークレット LCP: 0.33 / 0.32 / 0.34
  • 下層 Shiki シークレット CLS: 0 / 0 / 0
  • 下層 描画のみ LCP: 0.12 / 0.17 / 0.18
  • 下層 描画のみ CLS: 0 / 0 / 0

Astro MPA + React

  • トップページ LCP: 0.36 / 0.33 / 0.38
  • トップページ CLS: 0.05 / 0.05 / 0.05
  • 下層 Shiki LCP: 0.15 / 0.15 / 0.15
  • 下層 Shiki CLS: 0 / 0 / 0
  • 下層 Shiki シークレット LCP: 0.33 / 0.34 / 0.35
  • 下層 Shiki シークレット CLS: 0 / 0.01 / 0
  • 下層 描画のみ LCP: 0.17 / 0.21 / 0.20
  • 下層 描画のみ CLS: 0 / 0 / 0

今回も、3 回のみの計測という、証左には足りない数値です。体感的には、みんな爆速です。 なお、上記はあくまでも、個人の感想であることをご理解いただければ幸甚です。

【極限編】同一 Astro 上で Solid / Svelte / Vue / React それぞれの処理速度を計測してみた(Qwik は無し)

今までがおもしろかったので、外部ストアをたくさん連れてきて、走ってもらいました。 前回で計測実験は閉幕と言いましたが、つい、1 日ほど時間があったのでやってしまいました。

wings.hatenablog.com

wings.hatenablog.com

wings.hatenablog.com

wings.hatenablog.com

参加ストアは以下の 12 選手です。

  • Solid Signal
  • Solid Nano Stores
  • Svelte Runes
  • Svelte Nano Stores
  • Vue Ref
  • Vue Nano Stores
  • React uSES
  • React Jotai
  • React Legend State
  • React Valtio
  • React Zustand
  • React Nano Stores

Redux は包まないといけないので、包まずにいける上記と違い欠場、Recoil は React 19 以降で動かないので欠場、としました。 Qwik は、Context 必須なのでこちらも欠場です。

今回の計測は「ストアの極限バケツリレー」です。 1 コンポーネントの中で最大 50 回の連鎖を行い、それを 10 コンポーネント用意し、状態が数値と文字列双方を通すので、50 * 10 * 2 = 1000 回のまでやりました。 なお、意図的に 1 コンポーネント内の連鎖数は 2・10・50 と分けて計測します(50 なのは React の連鎖制限が 50 なので)。 各コンポーネントは Astro の client:only によって分離しています。

こんなコードは普段書かないので、あくまでも実験用です。

Solid / Svelte / Vue のグローバルストアについての詳細は、こちらを参照してください。

wings.hatenablog.com

この機構は、Nano Stores や React に対しても適用しました。

では、早速いきましょう。今回も import 文と Number03〜09・String03〜09 のコンポーネントは割愛します。また、他の割愛部分は、都度記載します。

共通

ここにある dragRaceMax の数値を先程の 2・10・50 に変えた上で計測します。

export const records = {
    SolidSignal: { start: 0, finished: 0 },
    SolidSignalNumber: { start: 0, finished: 0 },
    SolidSignalString: { start: 0, finished: 0 },

    SolidNano: { start: 0, finished: 0 },
    SolidNanoNumber: { start: 0, finished: 0 },
    SolidNanoString: { start: 0, finished: 0 },

    SvelteRunes: { start: 0, finished: 0 },
    SvelteRunesNumber: { start: 0, finished: 0 },
    SvelteRunesString: { start: 0, finished: 0 },

    SvelteNano: { start: 0, finished: 0 },
    SvelteNanoNumber: { start: 0, finished: 0 },
    SvelteNanoString: { start: 0, finished: 0 },

    VueRef: { start: 0, finished: 0 },
    VueRefNumber: { start: 0, finished: 0 },
    VueRefString: { start: 0, finished: 0 },

    VueNano: { start: 0, finished: 0 },
    VueNanoNumber: { start: 0, finished: 0 },
    VueNanoString: { start: 0, finished: 0 },

    ReactUses: { start: 0, finished: 0 },
    ReactUsesNumber: { start: 0, finished: 0 },
    ReactUsesString: { start: 0, finished: 0 },

    ReactJotai: { start: 0, finished: 0 },
    ReactJotaiNumber: { start: 0, finished: 0 },
    ReactJotaiString: { start: 0, finished: 0 },

    ReactLegend: { start: 0, finished: 0 },
    ReactLegendNumber: { start: 0, finished: 0 },
    ReactLegendString: { start: 0, finished: 0 },

    ReactValtio: { start: 0, finished: 0 },
    ReactValtioNumber: { start: 0, finished: 0 },
    ReactValtioString: { start: 0, finished: 0 },

    ReactZustand: { start: 0, finished: 0 },
    ReactZustandNumber: { start: 0, finished: 0 },
    ReactZustandString: { start: 0, finished: 0 },

    ReactNano: { start: 0, finished: 0 },
    ReactNanoNumber: { start: 0, finished: 0 },
    ReactNanoString: { start: 0, finished: 0 },
};
export type Records = keyof typeof records;
const now = () => performance.now();
export const recordStart = (target: Records) => {
    records[target].start = now();
};
export const recordFinished = (target: Records) => {
    if (records[target].start === 0) return;

    records[target].finished = now();
};
export const logTotal = (target: Records) => {
    const targetNumber = `${target}Number` as Records;
    const targetString = `${target}String` as Records;
    if (records[targetNumber].start === 0) return;

    const diffNumber = records[targetNumber].finished - records[targetNumber].start;
    const diffString = records[targetString].finished - records[targetString].start;
    console.log(`${targetNumber}: ${diffNumber} ミリ秒`);
    console.log(`${targetString}: ${diffString} ミリ秒`);
    console.log(`${target}: ${diffNumber + diffString} ミリ秒`);};
export const dragRaceMax = 50;

index.astro

<Layout title="Drag Race">
    <SolidSignal />
    <SolidNano />
    <SvelteRunes />
    <SvelteNano />
    <VueRef />
    <VueNano />
    <ReactUses />
    <ReactJotai />
    <ReactLegend />
    <ReactValtio />
    <ReactZustand />
    <ReactNano />
</Layout>

Solid Signal

SolidSignal.astro

<SolidSignalStart client:only="solid" />
<SolidSignalNumber01 client:only="solid" />
// 10 まで同じ構造が続きます
<SolidSignalString01 client:only="solid" />
// 10 まで同じ構造が続きます

ストア定義

type StartStore = 'number' | 'string' | null;
const initialStart = null as StartStore;
const solidStartStore = {
    server: {
        startStore: (): StartStore => initialStart,
        setStart: (_: StartStore) => {},
    },
    client: () => {
        const [ startStore, setStart ] = createSignal<StartStore>(initialStart);

        return { startStore, setStart };
    },
};

type NumberStore = number;
const initialNumber = 0 as NumberStore;
const solidNumberStore01 = {
    server: { numberStore01: (): NumberStore => initialNumber, setNumber01: (_: NumberStore) => {} },
    client: () => {
        const [ numberStore01, setNumber01 ] = createSignal<NumberStore>(initialNumber);

        return { numberStore01, setNumber01 };
    },
};
// 10 まで同じ構造が続きます

type StringStore = string;
const initialString = '' as StringStore;
const solidStringStore01 = {
    server: { stringStore01: (): StringStore => initialString, setString01: (_: StringStore) => {} },
    client: () => {
        const [ stringStore01, setString01 ] = createSignal<StringStore>(initialString);

        return { stringStore01, setString01 };
    },
};
// 10 まで同じ構造が続きます

export const _solidDragRace = {
    solidStart: solidStartStore,
    solidNumber01: solidNumberStore01,
    // 10 まで同じ構造が続きます
    solidString01: solidStringStore01,
    // 10 まで同じ構造が続きます
};

Start.tsx

export default function Start() {
    const { setStart } = useStore('solidStart');

    const raceStart = () => {
        setStart('number');
    };

    return (
        <fieldset>
            <button onClick={ raceStart }>Start Solid Signal!!!</button>
        </fieldset>
    )
}

Number01.tsx

export default function SignalNumber01() {
    const { startStore } = useStore('solidStart');
    const { numberStore01, setNumber01 } = useStore('solidNumber01');
    const { setNumber02 } = useStore('solidNumber02');

    createEffect(() => {
        if (startStore() !== 'number') return;

        recordStart('SolidSignalNumber');
        setNumber01(1);
    });

    createEffect(() => {
        if (!numberStore01()) return;

        setNumber01(numberStore01() + 1);
        if (numberStore01() === dragRaceMax) {
            setNumber01(0);
            setNumber02(1);
        }
    });

    return null;
}

Number02.tsx

export default function SignalNumber02() {
    const { numberStore02, setNumber02 } = useStore('solidNumber02');
    const { setNumber03 } = useStore('solidNumber03');

    createEffect(() => {
        if (!numberStore02()) return;

        setNumber02(numberStore02() + 1);
        if (numberStore02() === dragRaceMax) {
            setNumber02(0);
            setNumber03(1);
        }
    });

    return null;
}

Number10.tsx

export default function SignalNumber10() {
    const { numberStore10, setNumber10 } = useStore('solidNumber10');
    const { setStart } = useStore('solidStart');

    createEffect(() => {
        if (!numberStore10()) return;

        if (numberStore10() === dragRaceMax) {
            recordFinished('SolidSignalNumber');
            setNumber10(0);
            setStart('string');
        } else {
            setNumber10(numberStore10() + 1);
        }
    });

    return null;
}

String01.tsx

export default function SignalString01() {
    const { startStore } = useStore('solidStart');
    const { stringStore01, setString01 } = useStore('solidString01');
    const { setString02 } = useStore('solidString02');

    createEffect(() => {
        if (startStore() !== 'string') return;

        recordStart('SolidSignalString');
        setString01('1');
    });

    createEffect(() => {
        const value = +stringStore01();
        if (!value) return;

        setString01((value + 1).toString());
        if (value === dragRaceMax) {
            setString01('');
            setString02('1');
        }
    });

    return null;
}

String02.tsx

export default function SignalString02() {
    const { stringStore02, setString02 } = useStore('solidString02');
    const { setString03 } = useStore('solidString03');

    createEffect(() => {
        const value = +stringStore02();
        if (!value) return;

        setString02((value + 1).toString());
        if (value === dragRaceMax) {
            setString02('');
            setString03('1');
        }
    });

    return null;
}

String10.tsx

export default function SignalString10() {
    const { stringStore10, setString10 } = useStore('solidString10');
    const { setStart } = useStore('solidStart');

    createEffect(() => {
        const value = +stringStore10();
        if (!value) return;

        if (value === dragRaceMax) {
            recordFinished('SolidSignalString');
            logTotal('SolidSignal');
            setStart(null);
            setString10('');
        } else {
            setString10((value + 1).toString());
        }
    });

    return null;
}

Solid Nano Stores

SolidNano.astro

<SolidNanoStart client:only="solid" />
<SolidNanoNumber01 client:only="solid" />
// 10 まで同じ構造が続きます
<SolidNanoString01 client:only="solid" />
// 10 まで同じ構造が続きます

ストア定義

なお、Nano Stores の定義は Svelte / Vue / React でも全く同じため、これらでは省きます。

const serverStore = <T>(initialValue: T) => ({
    get value() { return initialValue },
    set: (newValue: T) => {},
    eq: (oldValue: T, newValue: T) => false,
    get: () =>  initialValue,
    get init() { return initialValue },
    get lc() { return initialValue },
    listen: (listener: (value: T, oldValue: T) => void) => () => {},
    notify: (oldValue?: T) => {},
    off: () => {},
    subscribe: (listener: (value: T, oldValue?: T) => void) => () => {},
});

type StartStore = 'number' | 'string' | null;
const initialStart = null as StartStore;
const nanoStartStore = {
    server: { startStore: serverStore(initialStart) },
    client: () => ({ startStore: atom(initialStart) }),
};

type NumberStore = number;
const initialNumber = 0 as NumberStore;
const nanoNumberStore01 = {
    server: { numberStore01: serverStore(initialNumber) },
    client: () => ({ numberStore01: atom(initialNumber) }),
};
// 10 まで同じ構造が続きます

type StringStore = string;
const initialString = '' as StringStore;
const nanoStringStore01 = {
    server: { stringStore01: serverStore(initialString) },
    client: () => ({ stringStore01: atom(initialString) }),
};
// 10 まで同じ構造が続きます

const nanoStores = {
    nanoStart: nanoStartStore,
    nanoNumber01: nanoNumberStore01,
    // 10 まで同じ構造が続きます
    nanoString01: nanoStringStore01,
    // 10 まで同じ構造が続きます
} as const;
export type NanoStores = typeof nanoStores;
export type NanoStoreKeys = keyof NanoStores;

const createServerStore = (key: NanoStoreKeys): NanoStores[typeof key]['server'] => nanoStores[key].server;

type CreateClientStore = ReturnType<NanoStores[NanoStoreKeys]['client']>;
const createClientStore = (key: NanoStoreKeys): CreateClientStore => nanoStores[key].client();

type ClientStoreInstance = Record<NanoStoreKeys, CreateClientStore | null>;
const clientStoreInstance = Object.fromEntries(Object.keys(nanoStores).map(key => ([ key, null ]))) as ClientStoreInstance;

export const useNanoStore = <K extends NanoStoreKeys, R extends ReturnType<NanoStores[K]['client']>>(key: K) => {
    if (typeof window === 'undefined') return createServerStore(key) as R;

    if (!clientStoreInstance[key]) {
        clientStoreInstance[key] = createClientStore(key);
    }

    return clientStoreInstance[key] as R;
};

Start.tsx

export default function Start() {
    const { startStore } = useNanoStore('nanoStart');

    const raceStart = () => {
        startStore.set('number');
    };
    return (
        <fieldset>
            <button onClick={ raceStart }>Start Solid Nano Stores!!!</button>
        </fieldset>
    )
}

Number01.tsx

export default function NanoNumber01() {
    const { startStore } = useNanoStore('nanoStart');
    const start = useStore(startStore);
    const { numberStore01 } = useNanoStore('nanoNumber01');
    const store = useStore(numberStore01);
    const { numberStore02 } = useNanoStore('nanoNumber02');

    createEffect(() => {
        if (start() !== 'number') return;

        recordStart('SolidNanoNumber');
        numberStore01.set(1);
    });

    createEffect(() => {
        if (!store()) return;

        numberStore01.set(store() + 1);
        if (store() === dragRaceMax) {
            numberStore01.set(0);
            numberStore02.set(1);
        }
    });

    return null;
}

Number02.tsx

export default function NanoNumber02() {
    const { numberStore02 } = useNanoStore('nanoNumber02');
    const store = useStore(numberStore02);
    const { numberStore03 } = useNanoStore('nanoNumber03');

    createEffect(() => {
        if (!store()) return;

        numberStore02.set(store() + 1);
        if (store() === dragRaceMax) {
            numberStore02.set(0);
            numberStore03.set(1);
        }
    });

    return null;
}

Number10.tsx

export default function NanoNumber10() {
    const { numberStore10 } = useNanoStore('nanoNumber10');
    const store = useStore(numberStore10);
    const { startStore } = useNanoStore('nanoStart');

    createEffect(() => {
        if (!store()) return;

        if (store() === dragRaceMax) {
            recordFinished('SolidNanoNumber');
            numberStore10.set(0);
            startStore.set('string');
        } else {
            numberStore10.set(store() + 1);
        }
    });

    return null;
}

String01.tsx

export default function NanoString01() {
    const { startStore } = useNanoStore('nanoStart');
    const start = useStore(startStore);
    const { stringStore01 } = useNanoStore('nanoString01');
    const store = useStore(stringStore01);
    const { stringStore02 } = useNanoStore('nanoString02');

    createEffect(() => {
        if (start() !== 'string') return;

        recordStart('SolidNanoString');
        stringStore01.set('1');
    });

    createEffect(() => {
        const value = +store();
        if (!value) return;

        stringStore01.set((value + 1).toString());
        if (value === dragRaceMax) {
            stringStore01.set('');
            stringStore02.set('1');
        }
    });

    return null;
}

String02.tsx

export default function NanoString02() {
    const { stringStore02 } = useNanoStore('nanoString02');
    const store = useStore(stringStore02);
    const { stringStore03 } = useNanoStore('nanoString03');

    createEffect(() => {
        const value = +store();
        if (!value) return;

        stringStore02.set((value + 1).toString());
        if (value === dragRaceMax) {
            stringStore02.set('');
            stringStore03.set('1');
        }
    });

    return null;
}

String10.tsx

export default function NanoString10() {
    const { stringStore10 } = useNanoStore('nanoString10');
    const store = useStore(stringStore10);
    const { startStore } = useNanoStore('nanoStart');

    createEffect(() => {
        const value = +store();
        if (!value) return;

        if (value === dragRaceMax) {
            recordFinished('SolidNanoString');
            logTotal('SolidNano');
            startStore.set(null);
            stringStore10.set('');
        } else {
            stringStore10.set((value + 1).toString());
        }
    });

    return null;
}

Svelte Runes

SvelteRunes.astro

<SvelteRunesStart client:only="svelte" />
<SvelteRunesNumber01 client:only="svelte" />
// 10 まで同じ構造が続きます
<SvelteRunesString01 client:only="svelte" />
// 10 まで同じ構造が続きます

ストア定義

type StartStore = 'number' | 'string' | null;
const initialStart = null as StartStore;
const svelteStartStore = {
    server: {
        startStore: (): StartStore => initialStart,
        setStart: (_: StartStore) => {},
    },
    client: () => {
        let startStore = $state<StartStore>(initialStart);
        const setStart = (value: StartStore) => { startStore = value; };

        return {
            startStore: () => startStore, setStart,
        };
    },
};

type NumberStore = number;
const initialNumber = 0 as NumberStore;
const svelteNumberStore01 = {
    server: { numberStore01: (): NumberStore => initialNumber, setNumber01: (_: NumberStore) => {} },
    client: () => {
        let numberStore01 = $state<NumberStore>(initialNumber);

        return { numberStore01: () => numberStore01, setNumber01: (value: NumberStore) => { numberStore01 = value; } };
    },
};
// 10 まで同じ構造が続きます

type StringStore = string;
const initialString = '' as StringStore;
const svelteStringStore01 = {
    server: { stringStore01: (): StringStore => initialString, setString01: (_: StringStore) => {} },
    client: () => {
        let stringStore01 = $state<StringStore>(initialString);

        return { stringStore01: () => stringStore01, setString01: (value: StringStore) => { stringStore01 = value; } };
    },
};
// 10 まで同じ構造が続きます

export const _svelteDragRace = {
    svelteStart: svelteStartStore,
    svelteNumber01: svelteNumberStore01,
    // 10 まで同じ構造が続きます
    svelteString01: svelteStringStore01,
    // 10 まで同じ構造が続きます
};

Start.svelte

<script lang="ts">
    const { setStart } = useStore('svelteStart');

    const raceStart = () => {
        setStart('number');
    };
</script>
<fieldset>
    <button onclick={ raceStart }>Start Svelte Runes!!!</button>
</fieldset>

Number01.svelte

<script lang="ts">
    const { startStore } = useStore('svelteStart');
    const { numberStore01, setNumber01 } = useStore('svelteNumber01');
    const { setNumber02 } = useStore('svelteNumber02');

    $effect(() => {
        if (startStore() !== 'number') return;

        recordStart('SvelteRunesNumber');
        setNumber01(1);
    });

    $effect(() => {
        if (!numberStore01()) return;

        setNumber01(numberStore01() + 1);
        if (numberStore01() === dragRaceMax) {
            setNumber01(0);
            setNumber02(1);
        }
    });
</script>

Number02.svelte

<script lang="ts">
    const { numberStore02, setNumber02 } = useStore('svelteNumber02');
    const { setNumber03 } = useStore('svelteNumber03');

    $effect(() => {
        if (!numberStore02()) return;

        setNumber02(numberStore02() + 1);
        if (numberStore02() === dragRaceMax) {
            setNumber02(0);
            setNumber03(1);
        }
    });
</script>

Number10.svelte

<script lang="ts">
    const { numberStore10, setNumber10 } = useStore('svelteNumber10');
    const { setStart } = useStore('svelteStart');

    $effect(() => {
        if (!numberStore10()) return;

        if (numberStore10() === dragRaceMax) {
            recordFinished('SvelteRunesNumber');
            setNumber10(0);
            setStart('string');
        } else {
            setNumber10(numberStore10() + 1);
        }
    });
</script>

String01.svelte

<script lang="ts">
    const { startStore } = useStore('svelteStart');
    const { stringStore01, setString01 } = useStore('svelteString01');
    const { setString02 } = useStore('svelteString02');

    $effect(() => {
        if (startStore() !== 'string') return;

        recordStart('SvelteRunesString');
        setString01('1');
    });

    $effect(() => {
        const value = +stringStore01();
        if (!value) return;

        setString01((value + 1).toString());
        if (value === dragRaceMax) {
            setString01('');
            setString02('1');
        }
    });
</script>

String02.svelte

<script lang="ts">
    const { stringStore02, setString02 } = useStore('svelteString02');
    const { setString03 } = useStore('svelteString03');

    $effect(() => {
        const value = +stringStore02();
        if (!value) return;

        setString02((value + 1).toString());
        if (value === dragRaceMax) {
            setString02('');
            setString03('1');
        }
    });
</script>

String10.svelte

<script lang="ts">
    const { stringStore10, setString10 } = useStore('svelteString10');
    const { setStart } = useStore('svelteStart');

    $effect(() => {
        const value = +stringStore10();
        if (!value) return;

        if (value === dragRaceMax) {
            recordFinished('SvelteRunesString');
            logTotal('SvelteRunes');
            setStart(null);
            setString10('');
        } else {
            setString10((value + 1).toString());
        }
    });
</script>

Svelte Nano Stores

SvelteNano.astro

<SvelteNanoStart client:only="svelte" />
<SvelteNanoNumber01 client:only="svelte" />
// 10 まで同じ構造が続きます
<SvelteNanoString01 client:only="svelte" />
// 10 まで同じ構造が続きます

ストア定義

Solid Nano Stores と同一です。

Start.svelte

<script lang="ts">
    const { startStore } = useNanoStore('nanoStart');

    const raceStart = () => {
        startStore.set('number');
    };
</script>
<fieldset>
    <button onclick={ raceStart }>Start Svelte Nano Stores!!!</button>
</fieldset>

Number01.svelte

<script lang="ts">
    const { startStore } = useNanoStore('nanoStart');
    const start = useStore(startStore);
    const { numberStore01 } = useNanoStore('nanoNumber01');
    const store = useStore(numberStore01);
    const { numberStore02 } = useNanoStore('nanoNumber02');

    $effect(() => {
        if (start.current !== 'number') return;

        recordStart('SvelteNanoNumber');
        numberStore01.set(1);
    });

    $effect(() => {
        if (!store.current) return;

        numberStore01.set(store.current + 1);
        if (store.current === dragRaceMax) {
            numberStore01.set(0);
            numberStore02.set(1);
        }
    });
</script>

Number02.svelte

<script lang="ts">
    const { numberStore02 } = useNanoStore('nanoNumber02');
    const store = useStore(numberStore02);
    const { numberStore03 } = useNanoStore('nanoNumber03');

    $effect(() => {
        if (!store.current) return;

        numberStore02.set(store.current + 1);
        if (store.current === dragRaceMax) {
            numberStore02.set(0);
            numberStore03.set(1);
        }
    });
</script>

Number10.svelte

<script lang="ts">
    const { numberStore10 } = useNanoStore('nanoNumber10');
    const store = useStore(numberStore10);
    const { startStore } = useNanoStore('nanoStart');

    $effect(() => {
        if (!store.current) return;

        if (store.current === dragRaceMax) {
            recordFinished('SvelteNanoNumber');
            numberStore10.set(0);
            startStore.set('string');
        } else {
            numberStore10.set(store.current + 1);
        }
    });
</script>

String01.svelte

<script lang="ts">
    const { startStore } = useNanoStore('nanoStart');
    const start = useStore(startStore);
    const { stringStore01 } = useNanoStore('nanoString01');
    const store = useStore(stringStore01);
    const { stringStore02 } = useNanoStore('nanoString02');

    $effect(() => {
        if (start.current !== 'string') return;

        recordStart('SvelteNanoString');
        stringStore01.set('1');
    });

    $effect(() => {
        const value = +store.current;
        if (!value) return;

        stringStore01.set((value + 1).toString());
        if (value === dragRaceMax) {
            stringStore01.set('');
            stringStore02.set('1');
        }
    });
</script>

String02.svelte

<script lang="ts">
    const { stringStore02 } = useNanoStore('nanoString02');
    const store = useStore(stringStore02);
    const { stringStore03 } = useNanoStore('nanoString03');

    $effect(() => {
        const value = +store.current;
        if (!value) return;

        stringStore02.set((value + 1).toString());
        if (value === dragRaceMax) {
            stringStore02.set('');
            stringStore03.set('1');
        }
    });
</script>

String10.svelte

<script lang="ts">
    const { stringStore10 } = useNanoStore('nanoString10');
    const store = useStore(stringStore10);
    const { startStore } = useNanoStore('nanoStart');

    $effect(() => {
        const value = +store.current;
        if (!value) return;

        if (value === dragRaceMax) {
            recordFinished('SvelteNanoString');
            logTotal('SvelteNano');
            startStore.set(null);
            stringStore10.set('');
        } else {
            stringStore10.set((value + 1).toString());
        }
    });
</script>

Vue Ref

VueRef.astro

<VueRefStart client:only="vue" />
<VueRefNumber01 client:only="vue" />
// 10 まで同じ構造が続きます
<VueRefString01 client:only="vue" />
// 10 まで同じ構造が続きます

ストア定義

type StartStore = 'number' | 'string' | null;
const initialStart = null as StartStore;
const refStartStore = {
    server: {
        startStore: (): StartStore => initialStart,
        setStart: (_: StartStore) => {},
    },
    client: () => {
        const startStore = ref<StartStore>(initialStart);
        const setStart = (value: StartStore) => { startStore.value = value; };

        return {
            startStore: () => startStore.value, setStart,
        };
    },
};

type NumberStore = number;
const initialNumber = 0 as NumberStore;
const refNumberStore01 = {
    server: { numberStore01: (): NumberStore => initialNumber, setNumber01: (_: NumberStore) => {} },
    client: () => {
        const numberStore01 = ref<NumberStore>(initialNumber);

        return { numberStore01: () => numberStore01.value, setNumber01: (value: NumberStore) => { numberStore01.value = value; } };
    },
};
// 10 まで同じ構造が続きます

type StringStore = string;
const initialString = '' as StringStore;
const refStringStore01 = {
    server: { stringStore01: (): StringStore => initialString, setNumber01: (_: StringStore) => {} },
    client: () => {
        const stringStore01 = ref<StringStore>(initialString);

        return { stringStore01: () => stringStore01.value, setNumber01: (value: StringStore) => { stringStore01.value = value; } };
    },
};
// 10 まで同じ構造が続きます

export const _vueDragRace = {
    vueStart: refStartStore,
    vueNumber01: refNumberStore01,
    // 10 まで同じ構造が続きます
    vueString01: refStringStore01,
    // 10 まで同じ構造が続きます
};

Start.vue

<script setup lang="ts">
   const { setStart } = useStore('vueStart');

   const raceStart = () => {
        setStart('number');
    };
</script>
<template>
    <fieldset>
        <button @click="raceStart">Start Vue Ref!!!</button>
    </fieldset>
</template>

Number01.vue

<script setup lang="ts">
   const { startStore } = useStore('vueStart');
   const { numberStore01, setNumber01 } = useStore('vueNumber01');
   const { setNumber02 } = useStore('vueNumber02');

   watch(
        () => startStore(),
        () => {
            if (startStore() !== 'number') return;

            recordStart('VueRefNumber');
            setNumber01(1);
        }, { flush: 'pre' }
    );

   watch(
        () => numberStore01(),
        () => {
            if (!numberStore01()) return;

            setNumber01(numberStore01() + 1);
            if (numberStore01() === dragRaceMax) {
                setNumber01(0);
                setNumber02(1);
            }
        }, { flush: 'pre' }
    );
</script>
<template>
    <!-- return null -->
</template>

Number02.vue

<script setup lang="ts">
   const { numberStore02, setNumber02 } = useStore('vueNumber02');
   const { setNumber03 } = useStore('vueNumber03');

   watch(
        () => numberStore02(),
        () => {
            if (!numberStore02()) return;

            setNumber02(numberStore02() + 1);
            if (numberStore02() === dragRaceMax) {
                setNumber02(0);
                setNumber03(1);
            }
        },
        { flush: 'pre' }
    );
</script>
<template>
    <!-- return null -->
</template>

Number10.vue

<script setup lang="ts">
   const { numberStore10, setNumber10 } = useStore('vueNumber10');
   const { setStart } = useStore('vueStart');

   watch(
        () => numberStore10(),
        () => {
            if (!numberStore10()) return;

            if (numberStore10() === dragRaceMax) {
                recordFinished('VueRefNumber');
                setNumber10(0);
                setStart('string');
            } else {
                setNumber10(numberStore10() + 1);
            }
        },
        { flush: 'pre' }
    );
</script>
<template>
    <!-- return null -->
</template>

String01.vue

<script setup lang="ts">
   const { startStore } = useStore('vueStart');
   const { stringStore01, setNumber01 } = useStore('vueString01');
   const { setNumber02 } = useStore('vueString02');

   watch(
        () => startStore(),
        () => {
            if (startStore() !== 'string') return;

            recordStart('VueRefString');
            setNumber01('1');
        }, { flush: 'pre' }
    );

   watch(
        () => stringStore01(),
        () => {
            const value = +stringStore01();
            if (!value) return;

            setNumber01((value + 1).toString());
            if (value === dragRaceMax) {
                setNumber01('');
                setNumber02('1');
            }
        }, { flush: 'pre' }
    );
</script>
<template>
    <!-- return null -->
</template>

String02.vue

<script setup lang="ts">
   const { stringStore02, setNumber02 } = useStore('vueString02');
   const { setNumber03 } = useStore('vueString03');

   watch(
        () => stringStore02(),
        () => {
            const value = +stringStore02();
            if (!value) return;

            setNumber02((value + 1).toString());
            if (value === dragRaceMax) {
                setNumber02('');
                setNumber03('1');
            }
        },
        { flush: 'pre' }
    );
</script>
<template>
    <!-- return null -->
</template>

String10.vue

<script setup lang="ts">
   const { stringStore10, setNumber10 } = useStore('vueString10');
   const { setStart } = useStore('vueStart');

   watch(
        () => stringStore10(),
        () => {
            const value = +stringStore10();
            if (!value) return;

            if (value === dragRaceMax) {
                recordFinished('VueRefString');
                logTotal('VueRef');
                setStart(null);
                setNumber10('');
            } else {
                setNumber10((value + 1).toString());
            }
        },
        { flush: 'pre' }
    );
</script>
<template>
    <!-- return null -->
</template>

Vue Nano Stores

VueNano.astro




ストア定義

Solid Nano Stores と同一です。

Start.vue

<script setup lang="ts">
   const { $startStore } = useNanoStore('nanoStart');

   const raceStart = () => {
        $startStore.set('number');
    };
</script>
<template>
    <fieldset>
        <button @click="raceStart">Start Vue Nano Stores!!!</button>
    </fieldset>
</template>

Number01.vue

<script setup lang="ts">
   const { $startStore } = useNanoStore('nanoStart');
   const start = useStore($startStore);
   const { $numberStore01 } = useNanoStore('nanoNumber01');
   const store = useStore($numberStore01);
   const { $numberStore02 } = useNanoStore('nanoNumber02');

   watch(
        () => start.value,
        () => {
            if (start.value !== 'number') return;

            recordStart('VueNanoNumber');
            $numberStore01.set(1);
        }, { flush: 'pre' }
    );

   watch(
        () => store.value,
        () => {
            if (!store.value) return;

            $numberStore01.set(store.value + 1);
            if (store.value === dragRaceMax) {
                $numberStore01.set(0);
                $numberStore02.set(1);
            }
        }, { flush: 'pre' }
    );
</script>
<template>
    <!-- return null -->
</template>

Number02.vue

<script setup lang="ts">
   const { $numberStore02 } = useNanoStore('nanoNumber02');
   const store = useStore($numberStore02);
   const { $numberStore03 } = useNanoStore('nanoNumber03');

   watch(
        () => store.value,
        () => {
            if (!store.value) return;

            $numberStore02.set(store.value + 1);
            if (store.value === dragRaceMax) {
                $numberStore02.set(0);
                $numberStore03.set(1);
            }
        },
        { flush: 'pre' }
    );
</script>
<template>
    <!-- return null -->
</template>

Number10.vue

<script setup lang="ts">
   const { $numberStore10 } = useNanoStore('nanoNumber10');
   const store = useStore($numberStore10);
   const { $startStore } = useNanoStore('nanoStart');

   watch(
        () => store.value,
        () => {
            if (!store.value) return;

            if (store.value === dragRaceMax) {
                recordFinished('VueNanoNumber');
                $numberStore10.set(0);
                $startStore.set('string');
            } else {
                $numberStore10.set(store.value + 1);
            }
        },
        { flush: 'pre' }
    );
</script>
<template>
    <!-- return null -->
</template>

String01.vue

<script setup lang="ts">
   const { $startStore } = useNanoStore('nanoStart');
   const start = useStore($startStore);
   const { $stringStore01 } = useNanoStore('nanoString01');
   const store = useStore($stringStore01);
   const { $stringStore02 } = useNanoStore('nanoString02');

   watch(
        () => start.value,
        () => {
            if (start.value !== 'string') return;

            recordStart('VueNanoString');
            $stringStore01.set('1');
        }, { flush: 'pre' }
    );

   watch(
        () => store.value,
        () => {
            const value = +store.value;
            if (!value) return;

            $stringStore01.set((value + 1).toString());
            if (value === dragRaceMax) {
                $stringStore01.set('');
                $stringStore02.set('1');
            }
        }, { flush: 'pre' }
    );
</script>
<template>
    <!-- return null -->
</template>

String02.vue

<script setup lang="ts">
   const { $stringStore02 } = useNanoStore('nanoString02');
   const store = useStore($stringStore02);
   const { $stringStore03 } = useNanoStore('nanoString03');

   watch(
        () => store.value,
        () => {
            const value = +store.value;
            if (!value) return;

            $stringStore02.set((value + 1).toString());
            if (value === dragRaceMax) {
                $stringStore02.set('');
                $stringStore03.set('1');
            }
        },
        { flush: 'pre' }
    );
</script>
<template>
    <!-- return null -->
</template>

String10.vue

<script setup lang="ts">
   const { $stringStore10 } = useNanoStore('nanoString10');
   const store = useStore($stringStore10);
   const { $startStore } = useNanoStore('nanoStart');

   watch(
        () => store.value,
        () => {
            const value = +store.value;
            if (!value) return;

            if (value === dragRaceMax) {
                recordFinished('VueNanoString');
                logTotal('VueNano');
                $startStore.set(null);
                $stringStore10.set('');
            } else {
                $stringStore10.set((value + 1).toString());
            }
        },
        { flush: 'pre' }
    );
</script>
<template>
    <!-- return null -->
</template>

React 用のストア供給ファイル

上記の Solid / Svelte / Vue 用の機構を React へ移植したものです。Zustand のみはやや細工が必要なので、ストア定義ファイルに個別に記載しています。

const allStores = {
    ..._uSESStores,
    ..._jotaiStores,
    ..._legendStores,
    ..._valtioStores,
    ..._nanoStores,
} as const;
export type AllStores = typeof allStores;
export type AllStoreKeys = keyof AllStores;

const createServerStore = (key: AllStoreKeys): AllStores[typeof key]['server'] => allStores[key].server;

type CreateClientStore = ReturnType<AllStores[AllStoreKeys]['client']>;
const createClientStore = (key: AllStoreKeys): CreateClientStore => allStores[key].client();

type ClientStoreInstance = Record<AllStoreKeys, CreateClientStore | null>;
const clientStoreInstance = Object.fromEntries(Object.keys(allStores).map(key => ([ key, null ]))) as ClientStoreInstance;

export const useReactStore = <K extends AllStoreKeys, R extends ReturnType<AllStores[K]['client']>>(key: K) => {
    if (typeof window === 'undefined') return createServerStore(key) as R;

    if (!clientStoreInstance[key]) {
        clientStoreInstance[key] = createClientStore(key);
    }

    return clientStoreInstance[key] as R;
};

React uSES

ReactUses.astro

<ReactUsesStart client:only="react" />
<ReactUsesNumber01 client:only="react" />
// 10 まで同じ構造が続きます
<ReactUsesString01 client:only="react" />
// 10 まで同じ構造が続きます

ストア定義

type Listener = () => void;
const serverStore = <T>(value: T) => ({
    getServerSnapshot() { return value; },
    getSnapshot() { return value; },
    subscribe(_: Listener) {},
    set(_: T) {},
});
const clientStore = <T>(initialValue: T) => () => {
    let store = initialValue;
    let listeners: Listener[] = [];
    const subscribe = (listener: Listener) => {
        listeners = [...listeners, listener];

        return () => {
            listeners = listeners.filter(l => l !== listener);
        };
    };
    const getServerSnapshot = () => initialValue;
    const getSnapshot = () => store;
    const set = (value: T) => {
        store = value;

        for (let listener of listeners) {
            listener();
        }
    };

    return { subscribe, getServerSnapshot, getSnapshot, set };
};

const initialStart = null as 'number' | 'string' | null;
const serverStartStore = { ...serverStore(initialStart) };
const usesStartStore = { server: serverStartStore, client: clientStore(initialStart) };

const initialNumber: number = 0;
const serverNumberStore = { ...serverStore(initialNumber) };
const usesNumberStore01 = { server: serverNumberStore, client: clientStore(initialNumber) };
// 10 まで同じ構造が続きます

const initialString: string = '';
const serverStringStore = { ...serverStore(initialString) };
const usesStringStore01 = { server: serverStringStore, client: clientStore(initialString) };
// 10 まで同じ構造が続きます

export const _uSESStores = {
    usesStart: usesStartStore,
    usesNumber01: usesNumberStore01,
    // 10 まで同じ構造が続きます
    usesString01: usesStringStore01,
    // 10 まで同じ構造が続きます
};

Start.tsx

export default function UsesStart() {
    const start = useReactStore('usesStart');

    const raceStart = () => {
        start.set('number');
    };

    return (
        <fieldset>
            <button onClick={ raceStart }>Start React uSES!!!</button>
        </fieldset>
    )
}

Number01.tsx

export default function UsesNumber01() {
    const start = useReactStore('usesStart');
    const startStore = useSyncExternalStore(start.subscribe, start.getSnapshot, start.getServerSnapshot);
    const store01 = useReactStore('usesNumber01');
    const storeValue = useSyncExternalStore(store01.subscribe, store01.getSnapshot, store01.getServerSnapshot);
    const store02 = useReactStore('usesNumber02');

    useLayoutEffect(() => {
        if (startStore !== 'number') return;

        recordStart('ReactUsesNumber');
        store01.set(1);
    }, [ startStore ]);

    useLayoutEffect(() => {
        if (!storeValue) return;

        store01.set(storeValue + 1);
        if (storeValue === dragRaceMax) {
            store01.set(0);
            store02.set(1);
        }
    }, [ storeValue ]);

    return null;
}

Number02.tsx

export default function UsesNumber02() {
    const store02 = useReactStore('usesNumber02');
    const storeValue = useSyncExternalStore(store02.subscribe, store02.getSnapshot, store02.getServerSnapshot);
    const store03 = useReactStore('usesNumber03');

    useLayoutEffect(() => {
        if (!storeValue) return;

        store02.set(storeValue + 1);
        if (storeValue === dragRaceMax) {
            store02.set(0);
            store03.set(1);
        }
    }, [ storeValue ]);

    return null;
}

Number10.tsx

export default function UsesNumber10() {
    const store10 = useReactStore('usesNumber10');
    const storeValue = useSyncExternalStore(store10.subscribe, store10.getSnapshot, store10.getServerSnapshot);
    const start = useReactStore('usesStart');

    useLayoutEffect(() => {
        if (!storeValue) return;

        if (storeValue === dragRaceMax) {
            recordFinished('ReactUsesNumber');
            store10.set(0);
            start.set('string');
        } else {
            store10.set(storeValue + 1);
        }
    }, [ storeValue ]);

    return null;
}

String01.tsx

export default function UsesString01() {
    const start = useReactStore('usesStart');
    const startStore = useSyncExternalStore(start.subscribe, start.getSnapshot, start.getServerSnapshot);
    const store01 = useReactStore('usesString01');
    const storeValue = useSyncExternalStore(store01.subscribe, store01.getSnapshot, store01.getServerSnapshot);
    const store02 = useReactStore('usesString02');

    useLayoutEffect(() => {
        if (startStore !== 'string') return;

        recordStart('ReactUsesString');
        store01.set('1');
    }, [ startStore ]);

    useLayoutEffect(() => {
        const value = +storeValue;
        if (!value) return;

        store01.set((value + 1).toString());
        if (value === dragRaceMax) {
            store01.set('');
            store02.set('1');
        }
    }, [ storeValue ]);

    return null;
}

String02.tsx

export default function UsesString02() {
    const store02 = useReactStore('usesString02');
    const storeValue = useSyncExternalStore(store02.subscribe, store02.getSnapshot, store02.getServerSnapshot);
    const store03 = useReactStore('usesString03');

    useLayoutEffect(() => {
        const value = +storeValue;
        if (!value) return;

        store02.set((value + 1).toString());
        if (value === dragRaceMax) {
            store02.set('');
            store03.set('1');
        }
    }, [ storeValue ]);

    return null;
}

String10.tsx

export default function UsesString10() {
    const store10 = useReactStore('usesString10');
    const storeValue = useSyncExternalStore(store10.subscribe, store10.getSnapshot, store10.getServerSnapshot);
    const start = useReactStore('usesStart');

    useLayoutEffect(() => {
        const value = +storeValue;
        if (!value) return;

        if (value === dragRaceMax) {
            recordFinished('ReactUsesString');
            logTotal('ReactUses');
            start.set(null);
            store10.set('');
        } else {
            store10.set((value + 1).toString());
        }
    }, [ storeValue ]);

    return null;
}

React Jotai

ReactJotai.astro

<ReactJotaiStart client:only="react" />
<ReactJotaiNumber01 client:only="react" />
// 10 まで同じ構造が続きます
<ReactJotaiString01 client:only="react" />
// 10 まで同じ構造が続きます

ストア定義

type StartStore = 'number' | 'string' | null;
const initialStart = null as StartStore;
const jotaiStartStore = {
    server: ({ startStore: initialStart }),
    client: () => ({ startStore: atom(initialStart) }),
};

type NumberStore = number;
const initialNumber = 0 as NumberStore;
const jotaiNumberStore01 = {
    server: ({ numberStore01: initialNumber }),
    client: () => ({ numberStore01: atom(initialNumber)}),
};
// 10 まで同じ構造が続きます

type StringStore = string;
const initialString = '' as StringStore;
const jotaiStringStore01 = {
    server: ({ stringStore01: initialString }),
    client: () => ({ stringStore01: atom(initialString)}),
};
// 10 まで同じ構造が続きます

export const _jotaiStores = {
    jotaiStart: jotaiStartStore,
    jotaiNumber01: jotaiNumberStore01,
    // 10 まで同じ構造が続きます
    jotaiString01: jotaiStringStore01,
    // 10 まで同じ構造が続きます
};

Start.tsx

export default function JotaiStart() {
    const { startStore } = useReactStore('jotaiStart');
    const [ _, setStart ] = useAtom(startStore);

    const raceStart = () => {
        setStart('number');
    };

    return (
        <fieldset>
            <button onClick={ raceStart }>Start React Jotai!!!</button>
        </fieldset>
    )
}

Number01.tsx

export default function JotaiNumber01() {
    const { startStore } = useReactStore('jotaiStart');
    const [ start ] = useAtom(startStore);
    const { numberStore01 } = useReactStore('jotaiNumber01');
    const [ store01, setNumber01 ] = useAtom(numberStore01);
    const { numberStore02 } = useReactStore('jotaiNumber02');
    const [ _, setNumber02 ] = useAtom(numberStore02);

    useLayoutEffect(() => {
        if (start !== 'number') return;

        recordStart('ReactJotaiNumber');
        setNumber01(1);
    }, [ start ]);

    useLayoutEffect(() => {
        if (!store01) return;

        setNumber01(store01 + 1);
        if (store01 === dragRaceMax) {
            setNumber01(0);
            setNumber02(1);
        }
    }, [ store01 ]);

    return null;
}

Number02.tsx

export default function JotaiNumber02() {
    const { numberStore02 } = useReactStore('jotaiNumber02');
    const [ store02, setNumber02 ] = useAtom(numberStore02);
    const { numberStore03 } = useReactStore('jotaiNumber03');
    const [ _, setNumber03 ] = useAtom(numberStore03);

    useLayoutEffect(() => {
        if (!store02) return;

        setNumber02(store02 + 1);
        if (store02 === dragRaceMax) {
            setNumber02(0);
            setNumber03(1);
        }
    }, [ store02 ]);

    return null;
}

Number10.tsx

export default function JotaiNumber10() {
    const { numberStore10 } = useReactStore('jotaiNumber10');
    const [ store10, setNumber10 ] = useAtom(numberStore10);
    const { startStore } = useReactStore('jotaiStart');
    const [ _, setStart ] = useAtom(startStore);

    useLayoutEffect(() => {
        if (!store10) return;

        if (store10 === dragRaceMax) {
            recordFinished('ReactJotaiNumber');
            setNumber10(0);
            setStart('string');
        } else {
            setNumber10(store10 + 1);
        }
    }, [ store10 ]);

    return null;
}

String01.tsx

export default function JotaiString01() {
    const { startStore } = useReactStore('jotaiStart');
    const [ start ] = useAtom(startStore);
    const { stringStore01 } = useReactStore('jotaiString01');
    const [ store01, setString01 ] = useAtom(stringStore01);
    const { stringStore02 } = useReactStore('jotaiString02');
    const [ _, setString02 ] = useAtom(stringStore02);

    useLayoutEffect(() => {
        if (start !== 'string') return;

        recordStart('ReactJotaiString');
        setString01('1');
    }, [ start ]);

    useLayoutEffect(() => {
        const value = +store01;
        if (!value) return;

        setString01((value + 1).toString());
        if (value === dragRaceMax) {
            setString01('');
            setString02('1');
        }
    }, [ store01 ]);

    return null;
}

String02.tsx

export default function JotaiString02() {
    const { stringStore02 } = useReactStore('jotaiString02');
    const [ store02, setString02 ] = useAtom(stringStore02);
    const { stringStore03 } = useReactStore('jotaiString03');
    const [ _, setString03 ] = useAtom(stringStore03);

    useLayoutEffect(() => {
        const value = +store02;
        if (!value) return;

        setString02((value + 1).toString());
        if (value === dragRaceMax) {
            setString02('');
            setString03('1');
        }
    }, [ store02 ]);

    return null;
}

String10.tsx

export default function JotaiString10() {
    const { stringStore10 } = useReactStore('jotaiString10');
    const [ store10, setString10 ] = useAtom(stringStore10);
    const { startStore } = useReactStore('jotaiStart');
    const [ start, setStart ] = useAtom(startStore);

    useLayoutEffect(() => {
        const value = +store10;
        if (!value) return;

        if (value === dragRaceMax) {
            recordFinished('ReactJotaiString');
            logTotal('ReactJotai');
            setStart(null);
            setString10('');
        } else {
            setString10((value + 1).toString());
        }
    }, [ store10 ]);

    return null;
}

React Legend State

ReactLegend.astro

<ReactLegendStart client:only="react" />
<ReactLegendNumber01 client:only="react" />
// 10 まで同じ構造が続きます
<ReactLegendString01 client:only="react" />
// 10 まで同じ構造が続きます

ストア定義

const serverStore = <T>(initialValue: T) => ({
    delete: () => {},
    set: (_: T) => {},
    peek: () => initialValue,
    get: (_?: TrackingType | GetOptions) => initialValue,
    onChange: (cb: ListenerFn<number>, options?: {}) => {},
});

type StartStore = 'number' | 'string' | null;
const initialStart = null as StartStore;
const legendStartStore$ = {
    server: ({ startStore$: serverStore(initialStart) }),
    client: () => ({ startStore$: observable<StartStore>(initialStart) }),
};

type NumberStore = number;
const initialNumber = 0 as NumberStore;
const legendNumberStore01$ = {
    server: ({ numberStore01$: serverStore(initialNumber) }),
    client: () => ({ numberStore01$: observable<NumberStore>(initialNumber) }),
};
// 10 まで同じ構造が続きます

type StringStore = string;
const initialString = '' as StringStore;
const legendStringStore01$ = {
    server: ({ stringStore01$: serverStore(initialString) }),
    client: () => ({ stringStore01$: observable<StringStore>(initialString) }),
};
// 10 まで同じ構造が続きます

export const _legendStores = {
    legendStart: legendStartStore$,
    legendNumber01: legendNumberStore01$,
    // 10 まで同じ構造が続きます
    legendString01: legendStringStore01$,
    // 10 まで同じ構造が続きます
};

Start.tsx

export default function LegendStart() {
    const { startStore$ } = useReactStore('legendStart');

    const raceStart = () => {
        startStore$.set('number');
    };

    return (
        <fieldset>
            <button onClick={ raceStart }>Start React Legend State!!!</button>
        </fieldset>
    )
}

Number01.tsx

export default function LegendNumber01() {
    const { startStore$ } = useReactStore('legendStart');
    const { numberStore01$ } = useReactStore('legendNumber01');
    const { numberStore02$ } = useReactStore('legendNumber02');

    useObserve(() => {
        if (startStore$.get() !== 'number') return;

        recordStart('ReactLegendNumber');
        numberStore01$.set(1);
    });

    useObserve(() => {
        const value = numberStore01$.get();
        if (!value) return;

        numberStore01$.set(value + 1);
        if (value === dragRaceMax) {
            numberStore01$.set(0);
            numberStore02$.set(1);
        }
    });

    return null;
}

Number02.tsx

export default function LegendNumber02() {
    const { numberStore02$ } = useReactStore('legendNumber02');
    const { numberStore03$ } = useReactStore('legendNumber03');

    useObserve(() => {
        const value = numberStore02$.get();
        if (!value) return;

        numberStore02$.set(value + 1);
        if (value === dragRaceMax) {
            numberStore02$.set(0);
            numberStore03$.set(1);
        }
    });

    return null;
}

Number10.tsx

export default function LegendNumber10() {
    const { numberStore10$ } = useReactStore('legendNumber10');
    const { startStore$ } = useReactStore('legendStart');

    useObserve(() => {
        const value = numberStore10$.get();
        if (!value) return;

        if (value === dragRaceMax) {
            recordFinished('ReactLegendNumber');
            numberStore10$.set(0);
            startStore$.set('string');
        } else {
            numberStore10$.set(value + 1);
        }
    });

    return null;
}

String01.tsx

export default function LegendString01() {
    const { startStore$ } = useReactStore('legendStart');
    const { stringStore01$ } = useReactStore('legendString01');
    const { stringStore02$ } = useReactStore('legendString02');

    useObserve(() => {
        if (startStore$.get() !== 'string') return;

        recordStart('ReactLegendString');
        stringStore01$.set('1');
    });

    useObserve(() => {
        const value = +stringStore01$.get();
        if (!value) return;

        stringStore01$.set((value + 1).toString());
        if (value === dragRaceMax) {
            stringStore01$.set('');
            stringStore02$.set('1');
        }
    });

    return null;
}

String02.tsx

export default function LegendString02() {
    const { stringStore02$ } = useReactStore('legendString02');
    const { stringStore03$ } = useReactStore('legendString03');

    useObserve(() => {
        const value = +stringStore02$.get();
        if (!value) return;

        stringStore02$.set((value + 1).toString());
        if (value === dragRaceMax) {
            stringStore02$.set('');
            stringStore03$.set('1');
        }
    });

    return null;
}

String10.tsx

export default function LegendString10() {
    const { stringStore10$ } = useReactStore('legendString10');
    const { startStore$ } = useReactStore('legendStart');

    useObserve(() => {
        const store = +stringStore10$.get();
        if (!store) return;

        if (store === dragRaceMax) {
            recordFinished('ReactLegendString');
            logTotal('ReactLegend');
            startStore$.set(null);
            stringStore10$.set('');
        } else {
            stringStore10$.set((store + 1).toString());
        }
    });

    return null;
}

React Valtio

ReactValtio.astro

<ReactValtioStart client:only="react" />
<ReactValtioNumber01 client:only="react" />
// 10 まで同じ構造が続きます
<ReactValtioString01 client:only="react" />
// 10 まで同じ構造が続きます

ストア定義

const serverStore = <T>(initialValue: T) => ({
    startStore: initialValue,
    setStart: (_: T) => {},
});

type StartStore = { value: 'number' | 'string' | null };
const initialStart: StartStore['value'] = null;
const valtioStartStore = {
    server: serverStore(initialStart),
    client: () => {
        const startStore = proxy<StartStore>({ value: initialStart });

        return { startStore, setStart: (value: StartStore['value']) => { startStore.value = value; } };
    },
};

type NumberStore = { value: number };
const initialNumber: NumberStore['value'] = 0;
const valtioNumberStore01 = {
    server: serverStore(initialNumber),
    client: () => {
        const numberStore01 = proxy<NumberStore>({ value: initialNumber });

        return { numberStore01, setNumber01: (value: NumberStore['value']) => { numberStore01.value = value; } };
    },
};
// 10 まで同じ構造が続きます

type StringStore = { value: string };
const initialString: StringStore['value'] = '';
const valtioStringStore01 = {
    server: serverStore(initialString),
    client: () => {
        const stringStore01 = proxy<StringStore>({ value: initialString });

        return { stringStore01, setString01: (value: StringStore['value']) => { stringStore01.value = value; } };
    },
};
// 10 まで同じ構造が続きます

export const _valtioStores = {
    valtioStart: valtioStartStore,
    valtioNumber01: valtioNumberStore01,
    // 10 まで同じ構造が続きます
    valtioString01: valtioStringStore01,
    // 10 まで同じ構造が続きます
};

Start.tsx

export default function ValtioStart() {
    const { setStart } = useReactStore('valtioStart');

    const raceStart = () => {
        setStart('number');
    };

    return (
        <fieldset>
            <button onClick={ raceStart }>Start React Valtio!!!</button>
        </fieldset>
    )
}

Number01.tsx

export default function ValtioNumber01() {
    const { startStore } = useReactStore('valtioStart');
    const start = useSnapshot(startStore);
    const { numberStore01, setNumber01 } = useReactStore('valtioNumber01');
    const store01 = useSnapshot(numberStore01);
    const { setNumber02 } = useReactStore('valtioNumber02');

    useLayoutEffect(() => {
        if (start.value !== 'number') return;

        recordStart('ReactValtioNumber');
        setNumber01(1);
    }, [ start.value ]);

    useLayoutEffect(() => {
        if (!store01.value) return;

        setNumber01(store01.value + 1);
        if (store01.value === dragRaceMax) {
            setNumber01(0);
            setNumber02(1);
        }
    }, [ store01.value ]);

    return null;
}

Number02.tsx

export default function ValtioNumber02() {
    const { numberStore02, setNumber02 } = useReactStore('valtioNumber02');
    const store02 = useSnapshot(numberStore02);
    const { setNumber03 } = useReactStore('valtioNumber03');

    useLayoutEffect(() => {
        if (!store02.value) return;

        setNumber02(store02.value + 1);
        if (store02.value === dragRaceMax) {
            setNumber02(0);
            setNumber03(1);
        }
    }, [ store02.value ]);

    return null;
}

Number10.tsx

export default function ValtioNumber10() {
    const { numberStore10, setNumber10 } = useReactStore('valtioNumber10');
    const store10 = useSnapshot(numberStore10);
    const { setStart } = useReactStore('valtioStart');

    useLayoutEffect(() => {
        if (!store10.value) return;

        if (store10.value === dragRaceMax) {
            recordFinished('ReactValtioNumber');
            setNumber10(0);
            setStart('string');
        } else {
            setNumber10(store10.value + 1);
        }
    }, [ store10.value ]);

    return null;
}

String01.tsx

export default function ValtioString01() {
    const { startStore } = useReactStore('valtioStart');
    const start = useSnapshot(startStore);
    const { stringStore01, setString01 } = useReactStore('valtioString01');
    const store01 = useSnapshot(stringStore01);
    const { setString02 } = useReactStore('valtioString02');

    useLayoutEffect(() => {
        if (start.value !== 'string') return;

        recordStart('ReactValtioString');
        setString01('1');
    }, [ start.value ]);

    useLayoutEffect(() => {
        const value = +store01.value;
        if (!value) return;

        setString01((value + 1).toString());
        if (value === dragRaceMax) {
            setString01('');
            setString02('1');
        }
    }, [ store01.value ]);

    return null;
}

String02.tsx

export default function ValtioString02() {
    const { stringStore02, setString02 } = useReactStore('valtioString02');
    const store02 = useSnapshot(stringStore02);
    const { setString03 } = useReactStore('valtioString03');

    useLayoutEffect(() => {
        const value = +store02.value;
        if (!value) return;

        setString02((value + 1).toString());
        if (value === dragRaceMax) {
            setString02('');
            setString03('1');
        }
    }, [ store02.value ]);

    return null;
}

String10.tsx

export default function ValtioString10() {
    const { stringStore10, setString10 } = useReactStore('valtioString10');
    const store10 = useSnapshot(stringStore10);
    const { setStart } = useReactStore('valtioStart');

    useLayoutEffect(() => {
        const value = +store10.value;
        if (!value) return;

        if (value === dragRaceMax) {
            recordFinished('ReactValtioString');
            logTotal('ReactValtio');
            setStart(null);
            setString10('');
        } else {
            setString10((value + 1).toString());
        }
    }, [ store10.value ]);

    return null;
}

React Zustand

ReactZustand.astro

<ReactZustandStart client:only="react" />
<ReactZustandNumber01 client:only="react" />
// 10 まで同じ構造が続きます
<ReactZustandString01 client:only="react" />
// 10 まで同じ構造が続きます

ストア定義

type StartStore = 'number' | 'string' | null;
const initialStart = null as StartStore;
const startServerStore = () => ({
    startStore: initialStart,
    setStart: (_: StartStore) => {},
});
const zustandStartStore = {
    server: startServerStore,
    client: () => {
        return create<ReturnType<typeof startServerStore>>(set => ({
            startStore: initialStart,
            setStart: (value: StartStore) => set({ startStore: value }),
        }));
    },
};

type NumberStore = number;
const initialNumber = 0 as NumberStore;
const numberServerStore01 = () => ({ numberStore01: initialNumber, setNumber01: (_: NumberStore) => {} });
const zustandNumberStore01 = {
    server: numberServerStore01,
    client: () => {
        return create<ReturnType<typeof numberServerStore01>>(set => ({
            numberStore01: initialNumber,
            setNumber01: (value: NumberStore) => set({ numberStore01: value }),
        }));
    },
};
// 10 まで同じ構造が続きます

type StringStore = string;
const initialString = '' as StringStore;
const stringServerStore01 = () => ({ stringStore01: initialString, setString01: (_: StringStore) => {} });
const zustandStringStore01 = {
    server: stringServerStore01,
    client: () => {
        return create<ReturnType<typeof stringServerStore01>>(set => ({
            stringStore01: initialString,
            setString01: (value: StringStore) => set({ stringStore01: value }),
        }));
    },
};
// 10 まで同じ構造が続きます

const zustandStores = {
    zustandStart: zustandStartStore,
    zustandNumber01: zustandNumberStore01,
    // 10 まで同じ構造が続きます
    zustandString01: zustandStringStore01,
    // 10 まで同じ構造が続きます
} as const;
export type ZustandStores = typeof zustandStores;
export type ZustandStoreKeys = keyof ZustandStores;

const createServerStore = (key: ZustandStoreKeys): ZustandStores[typeof key]['server'] => zustandStores[key].server;

type CreateClientStore = ReturnType<ZustandStores[ZustandStoreKeys]['client']>;
const createClientStore = (key: ZustandStoreKeys): CreateClientStore => zustandStores[key].client();

type ClientStoreInstance = Record<ZustandStoreKeys, CreateClientStore | null>;
const clientStoreInstance = Object.fromEntries(Object.keys(zustandStores).map(key => ([ key, null ]))) as ClientStoreInstance;

export const useZustandStore = <K extends ZustandStoreKeys, R extends ReturnType<ZustandStores[K]['server']>>(key: K) => {
    if (typeof window === 'undefined') return createServerStore(key) as R;

    if (!clientStoreInstance[key]) {
        clientStoreInstance[key] = createClientStore(key);
    }

    const useHook = clientStoreInstance[key]!;
    return useHook() as R;
};

Start.tsx

export default function ZustandStart() {
    const { setStart } = useZustandStore('zustandStart');

    const raceStart = () => {
        setStart('number');
    };

    return (
        <fieldset>
            <button onClick={ raceStart }>Start React Zustand!!!</button>
        </fieldset>
    )
}

Number01.tsx

export default function ZustandNumber01() {
    const { startStore } = useZustandStore('zustandStart');
    const { numberStore01, setNumber01 } = useZustandStore('zustandNumber01');
    const { setNumber02 } = useZustandStore('zustandNumber02');

    useLayoutEffect(() => {
        if (startStore !== 'number') return;

        recordStart('ReactZustandNumber');
        setNumber01(1);
    }, [ startStore ]);

    useLayoutEffect(() => {
        if (!numberStore01) return;

        setNumber01(numberStore01 + 1);
        if (numberStore01 === dragRaceMax) {
            setNumber01(0);
            setNumber02(1);
        }
    }, [ numberStore01 ]);

    return null;
}

Number02.tsx

export default function ZustandNumber02() {
    const { numberStore02, setNumber02 } = useZustandStore('zustandNumber02');
    const { setNumber03 } = useZustandStore('zustandNumber03');

    useLayoutEffect(() => {
        if (!numberStore02) return;

        setNumber02(numberStore02 + 1);
        if (numberStore02 === dragRaceMax) {
            setNumber02(0);
            setNumber03(1);
        }
    }, [ numberStore02 ]);

    return null;
}

Number10.tsx

export default function ZustandNumber10() {
    const { numberStore10, setNumber10 } = useZustandStore('zustandNumber10');
    const { setStart } = useZustandStore('zustandStart');

    useLayoutEffect(() => {
        if (!numberStore10) return;

        if (numberStore10 === dragRaceMax) {
            recordFinished('ReactZustandNumber');
            setNumber10(0);
            setStart('string');
        } else {
            setNumber10(numberStore10 + 1);
        }
    }, [ numberStore10 ]);

    return null;
}

String01.tsx

export default function ZustandString01() {
    const { startStore } = useZustandStore('zustandStart');
    const { stringStore01, setString01 } = useZustandStore('zustandString01');
    const { setString02 } = useZustandStore('zustandString02');

    useLayoutEffect(() => {
        if (startStore !== 'string') return;

        recordStart('ReactZustandString');
        setString01('1');
    }, [ startStore ]);

    useLayoutEffect(() => {
        const value = +stringStore01;
        if (!value) return;

        setString01((value + 1).toString());
        if (value === dragRaceMax) {
            setString01('');
            setString02('1');
        }
    }, [ stringStore01 ]);

    return null;
}

String02.tsx

export default function ZustandString02() {
    const { stringStore02, setString02 } = useZustandStore('zustandString02');
    const { setString03 } = useZustandStore('zustandString03');

    useLayoutEffect(() => {
        const value = +stringStore02;
        if (!value) return;

        setString02((value + 1).toString());
        if (value === dragRaceMax) {
            setString02('');
            setString03('1');
        }
    }, [ stringStore02 ]);

    return null;
}

String10.tsx

export default function ZustandString10() {
    const { stringStore10, setString10 } = useZustandStore('zustandString10');
    const { setStart } = useZustandStore('zustandStart');

    useLayoutEffect(() => {
        const value = +stringStore10;
        if (!value) return;

        if (value === dragRaceMax) {
            recordFinished('ReactZustandString');
            logTotal('ReactZustand');
            setStart(null);
            setString10('');
        } else {
            setString10((value + 1).toString());
        }
    }, [ stringStore10 ]);

    return null;
}

React Nano Stores

ReactNano.astro

<ReactNanoStart client:only="react" />
<ReactNanoNumber01 client:only="react" />
...
<ReactNanoString01 client:only="react" />
...

ストア定義

Solid Nano Stores と同一です。

Start.tsx

export default function NanoStart() {
    const { $startStore } = useReactStore('nanoStart');

    const raceStart = () => {
        $startStore.set('number');
    };

    return (
        <fieldset>
            <button onClick={ raceStart }>Start React Nano Stores!!!</button>
        </fieldset>
    )
}

Number01.tsx

export default function NanoNumber01() {
    const { $startStore } = useReactStore('nanoStart');
    const start = useStore($startStore, { ssr: 'initial' });
    const { $numberStore01 } = useReactStore('nanoNumber01');
    const store01 = useStore($numberStore01, { ssr: 'initial' });
    const { $numberStore02 } = useReactStore('nanoNumber02');

    useLayoutEffect(() => {
        if (start !== 'number') return;

        recordStart('ReactNanoNumber');
        $numberStore01.set(1);
    }, [ start ]);

    useLayoutEffect(() => {
        if (!store01) return;

        $numberStore01.set(store01 + 1);
        if (store01 === dragRaceMax) {
            $numberStore01.set(0);
            $numberStore02.set(1);
        }
    }, [ store01 ]);

    return null;
}

Number02.tsx

export default function NanoNumber02() {
    const { $numberStore02 } = useReactStore('nanoNumber02');
    const store02 = useStore($numberStore02, { ssr: 'initial' });
    const { $numberStore03 } = useReactStore('nanoNumber03');

    useLayoutEffect(() => {
        if (!store02) return;

        $numberStore02.set(store02 + 1);
        if (store02 === dragRaceMax) {
            $numberStore02.set(0);
            $numberStore03.set(1);
        }
    }, [ store02 ]);

    return null;
}

Number10.tsx

export default function NanoNumber10() {
    const { $numberStore10 } = useReactStore('nanoNumber10');
    const store10 = useStore($numberStore10, { ssr: 'initial' });
    const { $startStore } = useReactStore('nanoStart');

    useLayoutEffect(() => {
        if (!store10) return;

        if (store10 === dragRaceMax) {
            recordFinished('ReactNanoNumber');
            $numberStore10.set(0);
            $startStore.set('string');
        } else {
            $numberStore10.set(store10 + 1);
        }
    }, [ store10 ]);

    return null;
}

String01.tsx

export default function NanoString01() {
    const { $startStore } = useReactStore('nanoStart');
    const start = useStore($startStore, { ssr: 'initial' });
    const { $stringStore01 } = useReactStore('nanoString01');
    const store01 = useStore($stringStore01, { ssr: 'initial' });
    const { $stringStore02 } = useReactStore('nanoString02');

    useLayoutEffect(() => {
        if (start !== 'string') return;

        recordStart('ReactNanoString');
        $stringStore01.set('1');
    }, [ start ]);

    useLayoutEffect(() => {
        const value = +store01;
        if (!value) return;

        $stringStore01.set((value + 1).toString());
        if (value === dragRaceMax) {
            $stringStore01.set('');
            $stringStore02.set('1');
        }
    }, [ store01 ]);

    return null;
}

String02.tsx

export default function NanoString02() {
    const { $stringStore02 } = useReactStore('nanoString02');
    const store02 = useStore($stringStore02, { ssr: 'initial' });
    const { $stringStore03 } = useReactStore('nanoString03');

    useLayoutEffect(() => {
        const value = +store02;
        if (!value) return;

        $stringStore02.set((value + 1).toString());
        if (value === dragRaceMax) {
            $stringStore02.set('');
            $stringStore03.set('1');
        }
    }, [ store02 ]);

    return null;
}

String10.tsx

export default function NanoString10() {
    const { $stringStore10 } = useReactStore('nanoString10');
    const store10 = useStore($stringStore10, { ssr: 'initial' });
    const { $startStore } = useReactStore('nanoStart');

    useLayoutEffect(() => {
        const value = +store10;
        if (!value) return;

        if (value === dragRaceMax) {
            recordFinished('ReactNanoString');
            logTotal('ReactNano');
            $startStore.set(null);
            $stringStore10.set('');
        } else {
            $stringStore10.set((value + 1).toString());
        }
    }, [ store10 ]);

    return null;
}

計測結果

ウォームアップ前である リロード直後は除き、それ以降の計測値です。

Solid Signal

dragRaceMax = 2 1 回目

  • SolidSignalNumber: 0 ミリ秒
  • SolidSignalString: 0.09999996423721313 ミリ秒
  • SolidSignal: 0.09999996423721313 ミリ秒

2 回目

  • SolidSignalNumber: 0 ミリ秒
  • SolidSignalString: 0.10000002384185791 ミリ秒
  • SolidSignal: 0.10000002384185791 ミリ秒

3 回目

  • SolidSignalNumber: 0.09999996423721313 ミリ秒
  • SolidSignalString: 0.10000002384185791 ミリ秒
  • SolidSignal: 0.19999998807907104 ミリ秒

dragRaceMax = 10 1 回目

  • SolidSignalNumber: 0.19999998807907104 ミリ秒
  • SolidSignalString: 0 ミリ秒
  • SolidSignal: 0.19999998807907104 ミリ秒

2 回目

  • SolidSignalNumber: 0.19999998807907104 ミリ秒
  • SolidSignalString: 0.10000002384185791 ミリ秒
  • SolidSignal: 0.30000001192092896 ミリ秒

3 回目

  • SolidSignalNumber: 0 ミリ秒
  • SolidSignalString: 0.20000004768371582 ミリ秒
  • SolidSignal: 0.20000004768371582 ミリ秒

dragRaceMax = 50 1 回目

  • SolidSignalNumber: 0.3999999761581421 ミリ秒
  • SolidSignalString: 0.20000004768371582 ミリ秒
  • SolidSignal: 0.6000000238418579 ミリ秒

2 回目

  • SolidSignalNumber: 0.3999999761581421 ミリ秒
  • SolidSignalString: 0.40000003576278687 ミリ秒
  • SolidSignal: 0.800000011920929 ミリ秒

3 回目

  • SolidSignalNumber: 0.19999998807907104 ミリ秒
  • SolidSignalString: 0.3999999761581421 ミリ秒
  • SolidSignal: 0.5999999642372131 ミリ秒

完走平均 - dragRaceMax = 2: 0.1333333254 ミリ秒 - dragRaceMax = 10: 0.2333333492 ミリ秒 - dragRaceMax = 50: 0.6666666667 ミリ秒

Solid Nano Stores

dragRaceMax = 2 1 回目

  • SolidNanoNumber: 0.19999998807907104 ミリ秒
  • SolidNanoString: 0.19999998807907104 ミリ秒
  • SolidNano: 0.3999999761581421 ミリ秒

2 回目

  • SolidNanoNumber: 0.10000002384185791 ミリ秒
  • SolidNanoString: 0.19999998807907104 ミリ秒
  • SolidNano: 0.30000001192092896 ミリ秒

3 回目

  • SolidNanoNumber: 0 ミリ秒
  • SolidNanoString: 0.19999998807907104 ミリ秒
  • SolidNano: 0.19999998807907104 ミリ秒

dragRaceMax = 10 1 回目

  • SolidNanoNumber: 0.3999999761581421 ミリ秒
  • SolidNanoString: 0.30000001192092896 ミリ秒
  • SolidNano: 0.699999988079071 ミリ秒

2 回目

  • SolidNanoNumber: 0.3999999761581421 ミリ秒
  • SolidNanoString: 0.19999998807907104 ミリ秒
  • SolidNano: 0.5999999642372131 ミリ秒

3 回目

  • SolidNanoNumber: 0.19999998807907104 ミリ秒
  • SolidNanoString: 0.10000002384185791 ミリ秒
  • SolidNano: 0.30000001192092896 ミリ秒

dragRaceMax = 50 1 回目

  • SolidNanoNumber: 1 ミリ秒
  • SolidNanoString: 0.699999988079071 ミリ秒
  • SolidNano: 1.699999988079071 ミリ秒

2 回目

  • SolidNanoNumber: 1.100000023841858 ミリ秒
  • SolidNanoString: 0.6000000238418579 ミリ秒
  • SolidNano: 1.7000000476837158 ミリ秒

3 回目

  • SolidNanoNumber: 0.8999999761581421 ミリ秒
  • SolidNanoString: 0.7000000476837158 ミリ秒
  • SolidNano: 1.600000023841858 ミリ秒

完走平均 - dragRaceMax = 2: 0.2999999921 ミリ秒 - dragRaceMax = 10: 0.5333333214 ミリ秒 - dragRaceMax = 50: 1.6666666865 ミリ秒

Svelte Runes

dragRaceMax = 2 1 回目

  • SvelteRunesNumber: 0.20000004768371582 ミリ秒
  • SvelteRunesString: 0.19999998807907104 ミリ秒
  • SvelteRunes: 0.40000003576278687 ミリ秒

2 回目

  • SvelteRunesNumber: 0.30000001192092896 ミリ秒
  • SvelteRunesString: 0.40000003576278687 ミリ秒
  • SvelteRunes: 0.7000000476837158 ミリ秒

3 回目

  • SvelteRunesNumber: 0.19999998807907104 ミリ秒
  • SvelteRunesString: 0.19999998807907104 ミリ秒
  • SvelteRunes: 0.3999999761581421 ミリ秒

dragRaceMax = 10 1 回目

  • SvelteRunesNumber: 16.600000023841858 ミリ秒
  • SvelteRunesString: 41.69999998807907 ミリ秒
  • SvelteRunes: 58.30000001192093 ミリ秒

2 回目

  • SvelteRunesNumber: 12.900000035762787 ミリ秒
  • SvelteRunesString: 37.5 ミリ秒
  • SvelteRunes: 50.40000003576279 ミリ秒

3 回目

  • SvelteRunesNumber: 12.5 ミリ秒
  • SvelteRunesString: 37.09999996423721 ミリ秒
  • SvelteRunes: 49.59999996423721 ミリ秒

dragRaceMax = 50 1 回目

  • SvelteRunesNumber: 351.69999998807907 ミリ秒
  • SvelteRunesString: 759.8000000119209 ミリ秒
  • SvelteRunes: 1111.5 ミリ秒

2 回目

  • SvelteRunesNumber: 334.39999997615814 ミリ秒
  • SvelteRunesString: 754.9000000357628 ミリ秒
  • SvelteRunes: 1089.300000011921 ミリ秒

3 回目

  • SvelteRunesNumber: 338.9000000357628 ミリ秒
  • SvelteRunesString: 761.8999999761581 ミリ秒
  • SvelteRunes: 1100.800000011921 ミリ秒

完走平均 - dragRaceMax = 2: 0.5000000199 ミリ秒 - dragRaceMax = 10: 52.7666666706 ミリ秒 - dragRaceMax = 50: 1,100.5333333413 ミリ秒

Svelte Nano Stores

dragRaceMax = 2 1 回目

  • SvelteNanoNumber: 0.3999999761581421 ミリ秒
  • SvelteNanoString: 0.40000003576278687 ミリ秒
  • SvelteNano: 0.800000011920929 ミリ秒

2 回目

  • SvelteNanoNumber: 0.19999998807907104 ミリ秒
  • SvelteNanoString: 0.3999999761581421 ミリ秒
  • SvelteNano: 0.5999999642372131 ミリ秒

3 回目

  • SvelteNanoNumber: 0.3999999761581421 ミリ秒
  • SvelteNanoString: 0.5 ミリ秒
  • SvelteNano: 0.8999999761581421 ミリ秒

dragRaceMax = 10 1 回目

  • SvelteNanoNumber: 15.699999988079071 ミリ秒
  • SvelteNanoString: 41.60000002384186 ミリ秒
  • SvelteNano: 57.30000001192093 ミリ秒

2 回目

  • SvelteNanoNumber: 12.799999952316284 ミリ秒
  • SvelteNanoString: 37.19999998807907 ミリ秒
  • SvelteNano: 49.999999940395355 ミリ秒

3 回目

  • SvelteNanoNumber: 13.5 ミリ秒
  • SvelteNanoString: 37.30000001192093 ミリ秒
  • SvelteNano: 50.80000001192093 ミリ秒

dragRaceMax = 50 1 回目

  • SvelteNanoNumber: 353.0999999642372 ミリ秒
  • SvelteNanoString: 758.6999999880791 ミリ秒
  • SvelteNano: 1111.7999999523163 ミリ秒

2 回目

  • SvelteNanoNumber: 338.80000001192093 ミリ秒
  • SvelteNanoString: 744.1999999880791 ミリ秒
  • SvelteNano: 1083 ミリ秒

3 回目

  • SvelteNanoNumber: 334.80000001192093 ミリ秒
  • SvelteNanoString: 745.5 ミリ秒
  • SvelteNano: 1080.300000011921 ミリ秒

完走平均 - dragRaceMax = 2: 0.7666666508 ミリ秒 - dragRaceMax = 10: 52.6999999881 ミリ秒 - dragRaceMax = 50: 1,091.6999999881 ミリ秒

Vue Ref

dragRaceMax = 2 1 回目

  • VueRefNumber: 0.09999996423721313 ミリ秒
  • VueRefString: 0.10000002384185791 ミリ秒
  • VueRef: 0.19999998807907104 ミリ秒

2 回目

  • VueRefNumber: 0.10000002384185791 ミリ秒
  • VueRefString: 0.19999998807907104 ミリ秒
  • VueRef: 0.30000001192092896 ミリ秒

3 回目

  • VueRefNumber: 0.20000004768371582 ミリ秒
  • VueRefString: 0 ミリ秒
  • VueRef: 0.20000004768371582 ミリ秒

dragRaceMax = 10 1 回目

  • VueRefNumber: 0.30000001192092896 ミリ秒
  • VueRefString: 0.30000001192092896 ミリ秒
  • VueRef: 0.6000000238418579 ミリ秒

2 回目

  • VueRefNumber: 0.19999998807907104 ミリ秒
  • VueRefString: 0.19999998807907104 ミリ秒
  • VueRef: 0.3999999761581421 ミリ秒

3 回目

  • VueRefNumber: 0.30000001192092896 ミリ秒
  • VueRefString: 0.10000002384185791 ミリ秒
  • VueRef: 0.40000003576278687 ミリ秒

dragRaceMax = 50 1 回目

  • VueRefNumber: 0.3999999761581421 ミリ秒
  • VueRefString: 0.3999999761581421 ミリ秒
  • VueRef: 0.7999999523162842 ミリ秒

2 回目

  • VueRefNumber: 0.19999998807907104 ミリ秒
  • VueRefString: 0.19999998807907104 ミリ秒
  • VueRef: 0.3999999761581421 ミリ秒

3 回目

  • VueRefNumber: 0.20000004768371582 ミリ秒
  • VueRefString: 0.3999999761581421 ミリ秒
  • VueRef: 0.6000000238418579 ミリ秒

完走平均 - dragRaceMax = 2: 0.2333333492 ミリ秒 - dragRaceMax = 10: 0.4666666786 ミリ秒 - dragRaceMax = 50: 0.5999999841 ミリ秒

Vue Nano Stores

dragRaceMax = 2 1 回目

  • VueNanoNumber: 0.10000002384185791 ミリ秒
  • VueNanoString: 0.19999998807907104 ミリ秒
  • VueNano: 0.30000001192092896 ミリ秒

2 回目

  • VueNanoNumber: 0.09999996423721313 ミリ秒
  • VueNanoString: 0.19999998807907104 ミリ秒
  • VueNano: 0.2999999523162842 ミリ秒

3 回目

  • VueNanoNumber: 0.19999998807907104 ミリ秒
  • VueNanoString: 0.19999998807907104 ミリ秒
  • VueNano: 0.3999999761581421 ミリ秒

dragRaceMax = 10 1 回目

  • VueNanoNumber: 0.30000001192092896 ミリ秒
  • VueNanoString: 0.30000001192092896 ミリ秒
  • VueNano: 0.6000000238418579 ミリ秒

2 回目

  • VueNanoNumber: 0.09999996423721313 ミリ秒
  • VueNanoString: 0.10000002384185791 ミリ秒
  • VueNano: 0.19999998807907104 ミリ秒

3 回目

  • VueNanoNumber: 0.10000002384185791 ミリ秒
  • VueNanoString: 0.19999998807907104 ミリ秒
  • VueNano: 0.30000001192092896 ミリ秒

dragRaceMax = 50 1 回目

  • VueNanoNumber: 0.5999999642372131 ミリ秒
  • VueNanoString: 0.800000011920929 ミリ秒
  • VueNano: 1.399999976158142 ミリ秒

2 回目

  • VueNanoNumber: 0.699999988079071 ミリ秒
  • VueNanoString: 0.5999999642372131 ミリ秒
  • VueNano: 1.2999999523162842 ミリ秒

3 回目

  • VueNanoNumber: 0.7000000476837158 ミリ秒
  • VueNanoString: 0.5 ミリ秒
  • VueNano: 1.2000000476837158 ミリ秒

完走平均 - dragRaceMax = 2: 0.3333333135 ミリ秒 - dragRaceMax = 10: 0.3666666746 ミリ秒 - dragRaceMax = 50: 1.2999999921 ミリ秒

React uSES

dragRaceMax = 2 1 回目

  • ReactUsesNumber: 2.399999976158142 ミリ秒
  • ReactUsesString: 1.800000011920929 ミリ秒
  • ReactUses: 4.199999988079071 ミリ秒

2 回目

  • ReactUsesNumber: 1.399999976158142 ミリ秒
  • ReactUsesString: 1.5 ミリ秒
  • ReactUses: 2.899999976158142 ミリ秒

3 回目

  • ReactUsesNumber: 1.199999988079071 ミリ秒
  • ReactUsesString: 1.199999988079071 ミリ秒
  • ReactUses: 2.399999976158142 ミリ秒

dragRaceMax = 10 1 回目

  • ReactUsesNumber: 5.599999964237213 ミリ秒
  • ReactUsesString: 4.5 ミリ秒
  • ReactUses: 10.099999964237213 ミリ秒

2 回目

  • ReactUsesNumber: 4.199999988079071 ミリ秒
  • ReactUsesString: 3.399999976158142 ミリ秒
  • ReactUses: 7.599999964237213 ミリ秒

3 回目

  • ReactUsesNumber: 3.299999952316284 ミリ秒
  • ReactUsesString: 3.100000023841858 ミリ秒
  • ReactUses: 6.399999976158142 ミリ秒

dragRaceMax = 50 1 回目

  • ReactUsesNumber: 16.5 ミリ秒
  • ReactUsesString: 15.800000011920929 ミリ秒
  • ReactUses: 32.30000001192093 ミリ秒

2 回目

  • ReactUsesNumber: 15.800000011920929 ミリ秒
  • ReactUsesString: 15.5 ミリ秒
  • ReactUses: 31.30000001192093 ミリ秒

3 回目

  • ReactUsesNumber: 23.5 ミリ秒
  • ReactUsesString: 16.299999952316284 ミリ秒
  • ReactUses: 39.799999952316284 ミリ秒

完走平均 - dragRaceMax = 2: 3.1666666468 ミリ秒 - dragRaceMax = 10: 8.03333330154 ミリ秒 - dragRaceMax = 50: 34.4666666587 ミリ秒

React Jotai

dragRaceMax = 2 1 回目

  • ReactJotaiNumber: 3.099999964237213 ミリ秒
  • ReactJotaiString: 2.199999988079071 ミリ秒
  • ReactJotai: 5.299999952316284 ミリ秒

2 回目

  • ReactJotaiNumber: 2.699999988079071 ミリ秒
  • ReactJotaiString: 1.899999976158142 ミリ秒
  • ReactJotai: 4.599999964237213 ミリ秒

3 回目

  • ReactJotaiNumber: 2.200000047683716 ミリ秒
  • ReactJotaiString: 1.600000023841858 ミリ秒
  • ReactJotai: 3.8000000715255737 ミリ秒

dragRaceMax = 10 1 回目

  • ReactJotaiNumber: 6.100000023841858 ミリ秒
  • ReactJotaiString: 5 ミリ秒
  • ReactJotai: 11.100000023841858 ミリ秒

2 回目

  • ReactJotaiNumber: 6.199999988079071 ミリ秒
  • ReactJotaiString: 5.600000023841858 ミリ秒
  • ReactJotai: 11.800000011920929 ミリ秒

3 回目

  • ReactJotaiNumber: 6.900000035762787 ミリ秒
  • ReactJotaiString: 5.300000011920929 ミリ秒
  • ReactJotai: 12.200000047683716 ミリ秒

dragRaceMax = 50 1 回目

  • ReactJotaiNumber: 25.100000023841858 ミリ秒
  • ReactJotaiString: 26.5 ミリ秒
  • ReactJotai: 51.60000002384186 ミリ秒

2 回目

  • ReactJotaiNumber: 23.399999976158142 ミリ秒
  • ReactJotaiString: 23.700000047683716 ミリ秒
  • ReactJotai: 47.10000002384186 ミリ秒

3 回目

  • ReactJotaiNumber: 24.30000001192093 ミリ秒
  • ReactJotaiString: 22.5 ミリ秒
  • ReactJotai: 46.80000001192093 ミリ秒

完走平均 - dragRaceMax = 2: 4.5666666627 ミリ秒 - dragRaceMax = 10: 11.7000000278 ミリ秒 - dragRaceMax = 50: 48.5000000199 ミリ秒

React Legend State

dragRaceMax = 2 1 回目

  • ReactLegendNumber: 1 ミリ秒
  • ReactLegendString: 1.300000011920929 ミリ秒
  • ReactLegend: 2.300000011920929 ミリ秒

2 回目

  • ReactLegendNumber: 0.7999999523162842 ミリ秒
  • ReactLegendString: 1 ミリ秒
  • ReactLegend: 1.7999999523162842 ミリ秒

3 回目

  • ReactLegendNumber: 0.6000000238418579 ミリ秒
  • ReactLegendString: 0.800000011920929 ミリ秒
  • ReactLegend: 1.4000000357627869 ミリ秒

dragRaceMax = 10 1 回目

  • ReactLegendNumber: 5.700000047683716 ミリ秒
  • ReactLegendString: 9.699999988079071 ミリ秒
  • ReactLegend: 15.400000035762787 ミリ秒

2 回目

  • ReactLegendNumber: 5.400000035762787 ミリ秒
  • ReactLegendString: 9.399999976158142 ミリ秒
  • ReactLegend: 14.800000011920929 ミリ秒

3 回目

  • ReactLegendNumber: 5.199999988079071 ミリ秒
  • ReactLegendString: 9.5 ミリ秒
  • ReactLegend: 14.699999988079071 ミリ秒

dragRaceMax = 50 1 回目

  • ReactLegendNumber: 46.19999998807907 ミリ秒
  • ReactLegendString: 53.40000003576279 ミリ秒
  • ReactLegend: 99.60000002384186 ミリ秒

2 回目

  • ReactLegendNumber: 46.69999998807907 ミリ秒
  • ReactLegendString: 53 ミリ秒
  • ReactLegend: 99.69999998807907 ミリ秒

3 回目

  • ReactLegendNumber: 45.30000001192093 ミリ秒
  • ReactLegendString: 55.30000001192093 ミリ秒
  • ReactLegend: 100.60000002384186 ミリ秒

完走平均 - dragRaceMax = 2: 1.8333333333 ミリ秒 - dragRaceMax = 10: 14.9666666786 ミリ秒 - dragRaceMax = 50: 99.9666666786 ミリ秒

React Valtio

dragRaceMax = 2 1 回目

  • ReactValtioNumber: 3 ミリ秒
  • ReactValtioString: 2.600000023841858 ミリ秒
  • ReactValtio: 5.600000023841858 ミリ秒

2 回目

  • ReactValtioNumber: 3.199999988079071 ミリ秒
  • ReactValtioString: 2.199999988079071 ミリ秒
  • ReactValtio: 5.399999976158142 ミリ秒

3 回目

  • ReactValtioNumber: 2.600000023841858 ミリ秒
  • ReactValtioString: 2 ミリ秒
  • ReactValtio: 4.600000023841858 ミリ秒

dragRaceMax = 10 1 回目

  • ReactValtioNumber: 3.699999988079071 ミリ秒
  • ReactValtioString: 2.899999976158142 ミリ秒
  • ReactValtio: 6.599999964237213 ミリ秒

2 回目

  • ReactValtioNumber: 3.699999988079071 ミリ秒
  • ReactValtioString: 2.800000011920929 ミリ秒
  • ReactValtio: 6.5 ミリ秒

3 回目

  • ReactValtioNumber: 3.300000011920929 ミリ秒
  • ReactValtioString: 3.100000023841858 ミリ秒
  • ReactValtio: 6.400000035762787 ミリ秒

dragRaceMax = 50 1 回目

  • ReactValtioNumber: 12.899999976158142 ミリ秒
  • ReactValtioString: 10.899999976158142 ミリ秒
  • ReactValtio: 23.799999952316284 ミリ秒

2 回目

  • ReactValtioNumber: 13 ミリ秒
  • ReactValtioString: 12.199999988079071 ミリ秒
  • ReactValtio: 25.19999998807907 ミリ秒

3 回目

  • ReactValtioNumber: 11.699999988079071 ミリ秒
  • ReactValtioString: 10.100000023841858 ミリ秒
  • ReactValtio: 21.80000001192093 ミリ秒

完走平均 - dragRaceMax = 2: 5.2000000079 ミリ秒 - dragRaceMax = 10: 6.5 ミリ秒 - dragRaceMax = 50: 23.5999999841 ミリ秒

React Zustand

dragRaceMax = 2 1 回目

  • ReactZustandNumber: 2.300000011920929 ミリ秒
  • ReactZustandString: 1.899999976158142 ミリ秒
  • ReactZustand: 4.199999988079071 ミリ秒

2 回目

  • ReactZustandNumber: 3.300000011920929 ミリ秒
  • ReactZustandString: 3.100000023841858 ミリ秒
  • ReactZustand: 6.400000035762787 ミリ秒

3 回目

  • ReactZustandNumber: 1.899999976158142 ミリ秒
  • ReactZustandString: 1.600000023841858 ミリ秒
  • ReactZustand: 3.5 ミリ秒

dragRaceMax = 10 1 回目

  • ReactZustandNumber: 4.299999952316284 ミリ秒
  • ReactZustandString: 4.300000011920929 ミリ秒
  • ReactZustand: 8.599999964237213 ミリ秒

2 回目

  • ReactZustandNumber: 5.100000023841858 ミリ秒
  • ReactZustandString: 4.199999988079071 ミリ秒
  • ReactZustand: 9.300000011920929 ミリ秒

3 回目

  • ReactZustandNumber: 4.900000035762787 ミリ秒
  • ReactZustandString: 4.100000023841858 ミリ秒
  • ReactZustand: 9.000000059604645 ミリ秒

dragRaceMax = 50 1 回目

  • ReactZustandNumber: 26.100000023841858 ミリ秒
  • ReactZustandString: 19.399999976158142 ミリ秒
  • ReactZustand: 45.5 ミリ秒

2 回目

  • ReactZustandNumber: 21 ミリ秒
  • ReactZustandString: 19.80000001192093 ミリ秒
  • ReactZustand: 40.80000001192093 ミリ秒

3 回目

  • ReactZustandNumber: 26.599999964237213 ミリ秒
  • ReactZustandString: 19.5 ミリ秒
  • ReactZustand: 46.09999996423721 ミリ秒

完走平均 - dragRaceMax = 2: 4.7000000079 ミリ秒 - dragRaceMax = 10: 8.9666666786 ミリ秒 - dragRaceMax = 50: 44.1333333254 ミリ秒

React Nano Stores

dragRaceMax = 2 1 回目

  • ReactNanoNumber: 2.299999952316284 ミリ秒
  • ReactNanoString: 1.800000011920929 ミリ秒
  • ReactNano: 4.099999964237213 ミリ秒

2 回目

  • ReactNanoNumber: 2 ミリ秒
  • ReactNanoString: 1.5999999642372131 ミリ秒
  • ReactNano: 3.599999964237213 ミリ秒

3 回目

  • ReactNanoNumber: 2 ミリ秒
  • ReactNanoString: 1.5 ミリ秒
  • ReactNano: 3.5 ミリ秒

dragRaceMax = 10 1 回目

  • ReactNanoNumber: 5 ミリ秒
  • ReactNanoString: 3.5 ミリ秒
  • ReactNano: 8.5 ミリ秒

2 回目

  • ReactNanoNumber: 4.399999976158142 ミリ秒
  • ReactNanoString: 3.400000035762787 ミリ秒
  • ReactNano: 7.800000011920929 ミリ秒

3 回目

  • ReactNanoNumber: 6 ミリ秒
  • ReactNanoString: 4.600000023841858 ミリ秒
  • ReactNano: 10.600000023841858 ミリ秒

dragRaceMax = 50 1 回目

  • ReactNanoNumber: 16.30000001192093 ミリ秒
  • ReactNanoString: 15.900000035762787 ミリ秒
  • ReactNano: 32.200000047683716 ミリ秒

2 回目

  • ReactNanoNumber: 17.400000035762787 ミリ秒
  • ReactNanoString: 17.900000035762787 ミリ秒
  • ReactNano: 35.300000071525574 ミリ秒

3 回目

  • ReactNanoNumber: 21.400000035762787 ミリ秒
  • ReactNanoString: 17 ミリ秒
  • ReactNano: 38.40000003576279 ミリ秒

完走平均 - dragRaceMax = 2: 3.7333333095 ミリ秒 - dragRaceMax = 10: 8.9666666786 ミリ秒 - dragRaceMax = 50: 35.3000000517 ミリ秒

平均値ランキング

dragRaceMax = 2

  1. SolidSignal: 0.1333333254 ミリ秒
  2. VueRef: 0.2333333492 ミリ秒
  3. SolidNanoStores: 0.2999999921 ミリ秒
  4. VueNanoStores: 0.3333333135 ミリ秒
  5. SvelteRunes: 0.5000000199 ミリ秒
  6. SvelteNanoStores: 0.7666666508 ミリ秒
  7. ReactLegendState: 1.8333333333 ミリ秒
  8. ReactUses: 3.1666666468 ミリ秒
  9. ReactNanoStores: 3.7333333095 ミリ秒
  10. ReactJotai: 4.5666666627 ミリ秒
  11. ReactZustand: 4.7000000079 ミリ秒
  12. ReactValtio: 5.2000000079 ミリ秒

dragRaceMax = 10

  1. SolidSignal: 0.2333333492 ミリ秒
  2. VueNanoStores: 0.3666666746 ミリ秒
  3. VueRef: 0.4666666786 ミリ秒
  4. SolidNanoStores: 0.5333333214 ミリ秒
  5. ReactValtio: 6.5 ミリ秒
  6. ReactUses: 8.03333330154 ミリ秒
  7. ReactZustand / ReactNanoStores: 8.9666666786 ミリ秒
  8. ReactJotai: 11.7000000278 ミリ秒
  9. ReactLegendState: 14.9666666786 ミリ秒
  10. SvelteNanoStores: 52.6999999881 ミリ秒
  11. SvelteRunes: 52.7666666706 ミリ秒

dragRaceMax = 50

  1. VueRef: 0.5999999841 ミリ秒
  2. SolidSignal: 0.6666666667 ミリ秒
  3. VueNanoStores: 1.2999999921 ミリ秒
  4. SolidNanoStores: 1.6666666865 ミリ秒
  5. ReactValtio: 23.5999999841 ミリ秒
  6. ReactUses: 34.4666666587 ミリ秒
  7. ReactNanoStores: 35.3000000517 ミリ秒
  8. ReactZustand: 44.1333333254 ミリ秒
  9. ReactJotai: 48.5000000199 ミリ秒
  10. ReactLegendState: 99.9666666786 ミリ秒
  11. SvelteRunes: 1,100.5333333413 ミリ秒
  12. SvelteNanoStores: 1,091.6999999881 ミリ秒

今回も 3 回ずつの結果とは言え、有意な数値とは言えませんが、この結果になりました。 実に興味深い結果です。 もしかしたら、Svelte は、「この $effect() は、コンパイル時とちゃんと合ってるか?」ってのを毎回検証して、連鎖数が増えるほど、どんどん遅くなってる気がします。気がするだけで、実際どうかは分かりませんし、普通こんな極限状態で連鎖させません。 あと React Valtio は、少ない連鎖数の場合は最下位ですが、連鎖数が増えると React 内での最上位を守っていたり、逆に少ない連鎖数で LegendState は React 内で最上位だったのに、連鎖数が増えるとどんどん遅くなっていたのが驚きでした。

今度こそ、閉幕にしたいです。

SolidStart でディレクトリありサーバパス、かつ、公開用と展示用に分けたい

表題の要件があり、以下の対応をしました。

SolidStart のバージョンは @solidjs/start": "1.3.2" です。

設定

公開用の .env.web

MODE=web
PUBLIC_BASE=/web/

展示用の .env.store

MODE=store
PUBLIC_BASE=/store/

package.json

{
  ...
  "scripts": {
    "dev": "vinxi dev",
    "dev:store": "vinxi dev --mode store",
    "build": "vinxi build",
    "build:store": "vinxi build --mode store",
    ...
  },
  ...
}

app.config.ts

import { defineConfig } from "@solidjs/start/config";
import { dirname, resolve } from "node:path";
import { fileURLToPath } from "node:url";
import { config as loadEnv } from "dotenv";

const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
const isBuild = process.env.NODE_ENV === 'production' || process.argv.includes('build');

const mode = process.env.npm_lifecycle_event?.split(':')[1] ?? 'web';
loadEnv({ path: `.env.${mode}` });

const base = process.env.npm_lifecycle_event?.includes('build') ? process.env.PUBLIC_BASE : '/';

export default defineConfig({
    server: {
        baseURL: isBuild ? base : '/',
        prerender: {
            routes: ['/'],
        },
        output: {
            dir: `.output-${mode}`,
        },
    },
    vite: {
        resolve: {
            alias: {
                '@': resolve(__dirname, './src')
            }
        }
    }
});

これで、公開用は npm run dev で開発環境起動と npm run build で .output-web/ にビルドされ、サーバパスを /web/ として扱ってくれます。 展示用は npm run dev:store で開発環境起動と npm run build:store で .output-store/ にビルドされ、サーバパスを /store/ として扱ってくれます。

処理内

公開用と展示用で表示などを変えたり、画像のパス用に以下を使っています。

export const storeMode = import.meta.env.MODE === 'store';
export const baseURL = import.meta.env.SERVER_BASE_URL;

なお、npm scripts の --mode store を削ってしまうと、この import.meta.env.MODE に store が入らなくなります。 ただ、叩くのときにぼくはいつも npm run なので、process.env.npm_lifecycle_event?.split(':')[1] ?? 'web' この判定だけにしています。

そして、レイアウトコンポーネントでは以下にしました。

<Router
    base={ import.meta.env.SERVER_BASE_URL }
    root={props => (
        <MetaProvider>
            ...
        </MetaProvider>
    )}
>
    <FileRoutes />
</Router>

ページ遷移について

なお、ページ内の SPA 遷移は、<a href={ `${baseURL}about/` }リンク</a> の形式にしています。 普通に <A href="/about">リンク</A> にしても / を抜いた <A href="about">リンク</A> にしても、結果的に /web//about のように / が重なってしまいました(これは useNavigate() でも同じ挙動になりました)。

PUBLIC_BASE の末尾の / を消したり、Router コンポーネントの base の末尾の / を消すようにしたら、A コンポーネントも useNavigate() も普通に動くんですが、トップページにアクセスした際に末尾の / が消されてしまい、指定された URL と食い違うことになってしまうので、採用しませんでした。

たぶんなんですが、挙動から推測すると、A コンポーネントにした場合、以下の処理になったのかなと思いました。

  • href の頭に / がある → よし通れ
  • href の頭に / が無い → 馬鹿野郎 / を付けやがれ
  • どちらにせよ → SERVER_BASE_URL を頭に付けるぞ!

で、a 要素だとこんな感じかなと思いました。

  • href の頭からが SERVER_BASE_URL と同じ → よっしゃ SPA だな!
  • href の頭からが SERVER_BASE_URL と違う → どこか知らんがとりあえず飛ばすか

色々と試してみて A コンポーネントでなく普通の a 要素で安定したんですが、SolidStart は去年の 10 月にディレクトリありサーバパスでのビルドがうまくできなかったことを考えると、今後、ディレクトリありサーバパスでも A コンポーネントでうまくいくように期待してます。

また、ボタンを押した結果によってパスが変わるという部分があり、そこは処理を通してパスが判明するので useNavigate() を使っていたのですが、使えないことになったので、かなりクセの強いやり方で凌ぎました...。

export default function Buttons() {
    ...
    const [ resultUrl, setResultUrl ] = createSignal('');

    let elLink: HTMLAnchorElement | undefined;
    ...
    const handleClick = () => {
        if (!elLink) return;
        ...
        setResultUrl(`${baseURL}遷移先`);
        elLink.click();
    };
    ...
    return (
        <div class="buttons">
            ...
            <a ref={ elLink } href={ resultUrl() }></a>
        </div>
    )
}

以上は、ぼくの環境及び要件での方法ということをご理解いただければ幸甚です。

【番外編】 同一 Astro 上で Solid / Svelte / Vue / React それぞれの処理速度を計測してみた(おまけで Qwik)

今までの 3 回までがおもしろかったので、番外編としてやってみました。

wings.hatenablog.com

wings.hatenablog.com

wings.hatenablog.com

今回の計測は、「障害物競走」です。

  1. 第 3 回でやった Effect 系 10 連鎖
  2. 第 2 回でやった最上部から最深部への 10 コンポーネントのバケツリレー
  3. 第 3 回でやった最深部から最上部への伝播
  4. 第 3 回でやった Memo 系 10 連鎖
  5. 第 3 回でやった並列での Store 10 連鎖

各内容自体は、第 2 回と第 3 回の項目を踏襲しています。 Qwik は今回も Astro とは別に Qwik City 上で計測し、Store でのコンポーネント間共有をできないので、Qwik のみ Context なので、条件は揃っていませんが...。

また、今回は console.log() での出力を「障害物」として追加しました(DevTools は開いた状態で計測)。10 連鎖になる 3. 以外はこの障害物が 10 個、3. のみは単一伝播なので 1 個のみです。 バケツリレーでは props をそのまま渡さず、Memo 系で受けて障害物を捌いた後に次へ渡すようにしてみました。

なお、以前の計測結果から、以下を使用しました。

  • Solid: リアクティブは createSignal()、検知は createEffect()
  • Svelte: リアクティブは $state()、検知は $effect()、最深部からの伝播は $bindable() が機構として他は持たないので不使用
  • Vue: リアクティブは最速だった ref()、検知は依存が 1 つなら watch()・2 つなら watchEffect()
  • React: リアクティブは useState()、検知は最速だった useLayoutEffect()、バケツリレーにおいて毎回 props の変化によるレンダリングが発生するので、その場合に差分検出を要する memo() などのロス時間が上乗せされる最適化は無し
  • Qwik: リアクティブは useSignal()、検知は useVisibleTask$()

Solid / Svelte / Vue のグローバルストアについての詳細は、こちらを参照してください。

計測は、ウォームアップ後や QRL ダウンロード後にあたる、リロード直後から実行した後から、としました。

wings.hatenablog.com

もうひとつやっちゃいました。

wings.hatenablog.com

では、早速いきましょう。今回も import 文は割愛します。また、他の割愛部分は、都度記載します。

共通

毎度おなじみの計測方法です。

performance04.ts

export const records = {
    Race: {
        start: 0,
        finished: 0,
    },
    Effect: {
        start: 0,
        finished: 0,
    },
    Props: {
        start: 0,
        finished: 0,
    },
    Context: {
        start: 0,
        finished: 0,
    },
    Memo: {
        start: 0,
        finished: 0,
    },
    Store: {
        start: 0,
        finished: 0,
    },
};
export type Records = keyof typeof records;
const now = () => performance.now();
export const recordStart = (target: Records) => {
    records[target].start = now();
};
export const recordFinished = (target: Records, name?: string) => {
    if (records[target].start === 0) return;

    records[target].finished = now();
    const diff = records[target].finished - records[target].start;
    console.log(`${name ?? target}: ${diff} ミリ秒`);
};
export const log = (text: string) => {
    console.log(`${text} 通過!`);
};
export const logEffect = () => log('Effect 10 連鎖');
export const logProps = () => log('Props 10 連鎖');
export const logContext = () => log('Context');
export const logMemo = () => log('Memo 10 連鎖');
export const logStore = () => log('Store 10 連鎖');

performance04/Solid.astro

以下は Solid 版で、Svelte / Vue / React 共に同じ構成です。

<div id="performance">
    <Start client:load />
    <Effect client:only="solid" />
    <Parent client:only="solid" />
    <Memo client:only="solid" />
    <Quantum00 client:only="solid" />
    <Quantum01 client:only="solid" />
    <Quantum02 client:only="solid" />
    <Quantum03 client:only="solid" />
    <Quantum04 client:only="solid" />
    <Quantum05 client:only="solid" />
    <Quantum06 client:only="solid" />
    <Quantum07 client:only="solid" />
    <Quantum08 client:only="solid" />
    <Quantum09 client:only="solid" />
    <Quantum10 client:only="solid" />
</div>

CSS

前回と同様にこれだけです。

#performance {
    width: 750px;
    margin: 0 auto;
}

型定義

最深部へのバケツリレーと最上部で使うコンポーネント用にまとめます。

export type RelayData = number;
export type RelayProps = {
    relay: RelayData;
};

Solid 版

レース種別切り替え用グローバルストア設定

type RaceId = 'Effect' | 'Props' | 'Context' | 'Memo' | 'Store' | null;
const initialValue: RaceId = null;

export const _solidRaceId = {
    server: {
        solidRaceId: (): RaceId => initialValue,
        solidSetRaceId: (_: RaceId) => {},
    },
    client: () => {
        const [ solidRaceId, solidSetRaceId ] = createSignal<RaceId>(initialValue);

        return { solidRaceId, solidSetRaceId };
    },
};

Start.tsx

計測開始用コンポーネントです。

export default function Start() {
    const { solidSetRaceId } = useStore('solidRaceId');

    const handleClick = () => {
        recordStart('Race');
        solidSetRaceId('Effect');
    };

    return (
        <fieldset>
            <button onClick={ handleClick }>レーススタート!</button>
        </fieldset>
    )
}

Effect.tsx

Effect 系 10 連鎖用コンポーネントです。

export default function Effect() {
    const { solidRaceId, solidSetRaceId } = useStore('solidRaceId');
    const [ state01, setState01 ] = createSignal(0);
    // 10 まで同じ構造が続きます

    let num = 0;

    createEffect(() => {
        if (solidRaceId() !== 'Effect') return;

        recordStart('Effect');
        setState01(num + 1);
    });
    createEffect(() => {
        if (!state01()) return;

        logEffect();
        setState02(state01());
    });
    // 10 まで同じ構造が続きます
    createEffect(() => {
        if (!state10()) return;

        logEffect();
        recordFinished('Effect');
        num++;
        solidSetRaceId('Props');
    });

    return null;
}

最深部へと最上部への伝播用 Context 設定

export type RelayData = number;
export type RelayProps = {
    relay: RelayData;
};

const createRelayDataContexts = () => {
    const [ relayData, setRelayData ] = createSignal<RelayData>(0);
    const setRelayDataWithLog = (value: RelayData = 0) => {
        logContext();
        setRelayData(value);
    };

    return { relayData, setRelayData, setRelayDataWithLog };
};
type RelayDataContextsType = ReturnType<typeof createRelayDataContexts>;
const RelayDataContexts = createContext<RelayDataContextsType>() as Context<RelayDataContextsType>;

export const RelayDataProvider = (props: { children: JSX.Element }) => {
    return (
        <RelayDataContexts.Provider value={ createRelayDataContexts() }>{ props.children }</RelayDataContexts.Provider>
    );
}
export const useRelayDataContexts = () => useContext(RelayDataContexts);

Parent.tsx

Context で包む親コンポーネントです。

export default function Parent() {
    return (
        <RelayDataProvider>
            <Child00 />
        </RelayDataProvider>
    )
};

Child00.tsx

最上部からのバケツリレーの開始地点と、最深部からの Context 伝播の終了地点のコンポーネントです。

export default function Child00() {
    const { relayData, setRelayData } = useRelayDataContexts();
    const { solidRaceId, solidSetRaceId } = useStore('solidRaceId');
    const [ relayStart, setRelayStart ] = createSignal(0);

    createEffect(() => {
        if (solidRaceId() !== 'Props' || relayData()) return;

        recordStart('Props');
        setRelayStart(1);
    });

    createEffect(() => {
        if (solidRaceId() !== 'Context' || !relayData()) return;

        recordFinished('Context');
        setRelayData(0);
        setRelayStart(0);

        solidSetRaceId('Memo');
    });

    return <Child01 relay={ relayStart() } />;
};

Child01.tsx

中継地点となる子コンポーネントです(Child09.tsx まで割愛します)

export default function Child01(props: RelayProps) {
    const relayData = createMemo(() => {
        if (!props.relay) return 0;

        logProps();
        return props.relay;
    });

    return <Child02 relay={ relayData() } />;
};

Child10.tsx

最上部からのバケツリレーの終了地点と、最深部からの Context 伝播の開始地点のコンポーネントです。

export default function Child10(props: RelayProps) {
    const { solidRaceId, solidSetRaceId } = useStore('solidRaceId');
    const { setRelayDataWithLog } = useRelayDataContexts();

    const relayData = createMemo(() => {
        if (!props.relay) return 0;

        logProps();
        return props.relay;
    });

    createEffect(() => {
        if (solidRaceId() !== 'Props' || !relayData()) return;

        recordFinished('Props');

        solidSetRaceId('Context');
        recordStart('Context');
        setRelayDataWithLog(1);
    });

    return null;
};

Memo.tsx

Memo 系 10 連鎖用コンポーネントです。

export default function Memo() {
    const { solidRaceId, solidSetRaceId } = useStore('solidRaceId');

    let num = 0;

    const state01 = createMemo(() => {
        if (solidRaceId() !== 'Memo') return 0;

        recordStart('Memo');
        return num + 1;
    });
    const state02 = createMemo(() => {
        if (!state01()) return;

        logMemo();
        return state01();
    });
    // 10 まで同じ構造が続きます
    const finished = createMemo(() => {
        if (!state10()) return;

        logMemo();
        recordFinished('Memo');
        return state10();
    });
    createEffect(() => {
        if (!finished()) return;

        num = finished() ?? 0;
        solidSetRaceId('Store');
    });

    return null;
}

Store 10 連鎖用グローバルストア設定

type Quantum = number;
const initialValue: Quantum = 0;

export const _solidQuantum = {
    server: {
        solidQuantum01: (): Quantum => initialValue,
        solidSetQuantum01: (_: Quantum) => {},
        // 10 まで同じ構造が続きます
    },
    client: () => {
        const [ solidQuantum01, solidSetQuantum01 ] = createSignal<Quantum>(initialValue);
        // 10 まで同じ構造が続きます

        return {
            solidQuantum01, solidSetQuantum01,
            // 10 まで同じ構造が続きます
        };
    },
};

Quantum00.tsx

Store 10 連鎖の計測開始用コンポーネントです。

export default function Quantum00() {
    const { solidRaceId } = useStore('solidRaceId');
    const { solidSetQuantum01 } = useStore('solidQuantum');

    let num = 0;

    createEffect(() => {
        if (solidRaceId() !== 'Store') return;

        recordStart('Store');
        solidSetQuantum01(num + 1);
        num++;
    });

    return null;
}

Quantum01.tsx

中継地点のコンポーネントです(Quantum09.tsx まで省きます)

export default function Quantum01() {
    const { solidQuantum01, solidSetQuantum02 } = useStore('solidQuantum');

    const relayData = createMemo(() => {
        if (!solidQuantum01()) return 0;

        logStore();
        return solidQuantum01();
    });

    createEffect(() => {
        if (!relayData()) return;

        solidSetQuantum02(relayData());
    });

    return null;
}

Quantum10.tsx

Store 10 連鎖の計測終了用コンポーネントです。

export default function Quantum10() {
    const { solidQuantum10 } = useStore('solidQuantum');

    const relayData = createMemo(() => {
        if (!solidQuantum10()) return 0;

        logStore();
        return solidQuantum10();
    });

    createEffect(() => {
        if (!relayData()) return;

        recordFinished('Store');
        recordFinished('Race');
    });

    return null;
}

Svelte 版

レース種別切り替え用グローバルストア設定

type RaceId = 'Effect' | 'Props' | 'Context' | 'Memo' | 'Store' | null;
const initialValue: RaceId = null;

export const _svelteRaceId = {
    server: {
        svelteRaceId: (): RaceId => initialValue,
        svelteSetRaceId: (_: RaceId) => {},
    },
    client: () => {
        let svelteRaceId = $state<RaceId>(initialValue);
        const svelteSetRaceId = (value: RaceId) => { svelteRaceId = value; };

        return { svelteRaceId: () => svelteRaceId, svelteSetRaceId };
    },
};

Start.svelte

計測開始用コンポーネントです。

<script lang="ts">
    const { svelteSetRaceId } = useStore('svelteRaceId');

    const handleClick = () => {
        recordStart('Race');
        svelteSetRaceId('Effect');
    };
</script>
<fieldset>
    <button onclick={ handleClick }>レーススタート!</button>
</fieldset>

Effect.svelte

<script lang="ts">
    const { svelteRaceId, svelteSetRaceId } = useStore('svelteRaceId');

    let state01 = $state(0);
    // 10 まで同じ構造が続きます

    let num = 0;

    $effect(() => {
        if (svelteRaceId() !== 'Effect') return;

        recordStart('Effect');
        state01 = num + 1;
    });
    $effect(() => {
        if (!state01) return;

        logEffect();
        state02 = state01;
    });
    // 10 まで同じ構造が続きます
    $effect(() => {
        if (!state10) return;

        logEffect();
        recordFinished('Effect');
        num++;
        svelteSetRaceId('Props');
    });
</script>

最深部へと最上部への伝播用 Context 設定

const createRelayDataContexts = () => {
    const states = $state({
        relayData: 0,
    });
    type States = typeof states;

    return {
        relayData: () => states.relayData,
        setRelayData: (value: States['relayData'] = 0) => states.relayData = value,
        setRelayDataWithLog: (value: States['relayData'] = 0) => {
            logContext();
            states.relayData = value;
        },
    };
};
const [ useRelayDataContexts, setRelayDataContexts ] = createContext<ReturnType<typeof createRelayDataContexts>>();

export type RelayProps = {
    relay: ReturnType<ReturnType<typeof createRelayDataContexts>['relayData']>;
}
export const provideRelayDataContexts = () => setRelayDataContexts(createRelayDataContexts());
export { useRelayDataContexts };

Parent.svelte

<script lang="ts">
    provideRelayDataContexts();
</script>
<Child00 />

Child00.svelte

<script lang="ts">
    const { relayData, setRelayData } = useRelayDataContexts();
    const { svelteRaceId, svelteSetRaceId } = useStore('svelteRaceId');
    let relayStart = $state(0);

    $effect(() => {
        if (svelteRaceId() !== 'Props' || relayData()) return;

        recordStart('Props');
        relayStart = 1;
    });

    $effect(() => {
        if (svelteRaceId() !== 'Context' || !relayData()) return;

        recordFinished('Context');
        setRelayData(0);
        relayStart = 0;

        svelteSetRaceId('Memo');
    });
</script>
<Child01 relay={ relayStart } />

Child01.svelte

<script lang="ts">
    const { relay }: RelayProps = $props();

    const relayData = $derived.by(() => {
        if (!relay) return 0;

        logProps();

        return relay;
    });
</script>
<Child02 relay={ relayData } />

Child10.svelte

<script lang="ts">
    const { relay }: RelayProps = $props();

    const relayData = $derived.by(() => {
        if (!relay) return 0;

        logProps();

        return relay;
    });

    const { svelteRaceId, svelteSetRaceId } = useStore('svelteRaceId');
    const { setRelayDataWithLog } = useRelayDataContexts();

    $effect(() => {
        if (svelteRaceId() !== 'Props' || !relayData) return;

        recordFinished('Props');

        svelteSetRaceId('Context');
        recordStart('Context');
        setRelayDataWithLog(1);
    });
</script>

Memo.svelte

<script lang="ts">
    const { svelteRaceId, svelteSetRaceId } = useStore('svelteRaceId');

    let num = 0;

    const state01 = $derived.by(() => {
        if (svelteRaceId() !== 'Memo') return 0;
        recordStart('Memo');
        return num + 1;
    });
    const state02 = $derived.by(() => {
        if (!state01) return;

        logMemo();
        return state01;
    });
    // 10 まで同じ構造が続きます
    const finished = $derived.by(() => {
        if (!state10) return;

        logMemo();
        recordFinished('Memo');
        return state10;
    });
    $effect(() => {
        if (!finished) return;

        num = finished;
        svelteSetRaceId('Store');
    });
</script>

Store 10 連鎖用グローバルストア設定

type Quantum = number;
const initialValue: Quantum = 0;

export const _svelteQuantum = {
    server: {
        svelteQuantum01: (): Quantum => initialValue,
        svelteSetQuantum01: (_: Quantum) => {},
        // 10 まで同じ構造が続きます
    },
    client: () => {
        let svelteQuantum01 = $state<Quantum>(initialValue);
        const svelteSetQuantum01 = (value: Quantum) => { svelteQuantum01 = value; };
        // 10 まで同じ構造が続きます

        return {
            svelteQuantum01: () => svelteQuantum01, svelteSetQuantum01,
            // 10 まで同じ構造が続きます
        };
    },
};

Quantum00.svelte

<script lang="ts">
    const { svelteRaceId } = useStore('svelteRaceId');
    const { svelteSetQuantum01 } = useStore('svelteQuantum');

    let num = 0;

    $effect(() => {
        if (svelteRaceId() !== 'Store') return;

        recordStart('Store');
        svelteSetQuantum01(num + 1);
        num++;
    });
</script>

Quantum01.svelte

<script lang="ts">
    const { svelteQuantum01, svelteSetQuantum02 } = useStore('svelteQuantum');

    const relayData = $derived.by(() => {
        if (!svelteQuantum01()) return 0;

        logStore();

        return svelteQuantum01();
    });

    $effect(() => {
        if (!relayData) return;

        svelteSetQuantum02(relayData);
    });
</script>

Quantum10.svelte

<script lang="ts">
    const { svelteQuantum10 } = useStore('svelteQuantum');
    const { svelteSetRaceId } = useStore('svelteRaceId');

    const relayData = $derived.by(() => {
        if (!svelteQuantum10()) return 0;

        logStore();

        return svelteQuantum10();
    });

    $effect(() => {
        if (!relayData) return;

        recordFinished('Store');
        recordFinished('Race');
        svelteSetRaceId(null);
    });
</script>

Vue 版

レース種別切り替え用グローバルストア設定

type RaceId = 'Effect' | 'Props' | 'Context' | 'Memo' | 'Store' | null;
const initialValue: RaceId = null;

export const _vueRaceId = {
    server: {
        vueRaceId: (): RaceId => initialValue,
        vueSetRaceId: (_: RaceId) => {},
    },
    client: () => {
        const vueRaceId = ref<RaceId>(initialValue);
        const vueSetRaceId = (value: RaceId) => { vueRaceId.value = value; };

        return { vueRaceId: () => vueRaceId.value, vueSetRaceId };
    },
};

Start.vue

<script setup lang="ts">
   const { vueSetRaceId } = useStore('vueRaceId');

   const handleClick = () => {
        recordStart('Race');
        vueSetRaceId('Effect');
    };
</script>
<template>
    <fieldset>
        <button @click="handleClick">レーススタート!</button>
    </fieldset>
</template>

Effect.vue

<script setup lang="ts">
   const { vueRaceId, vueSetRaceId } = useStore('vueRaceId');

   const state01 = ref(0);
   // 10 まで同じ構造が続きます

   let num = 0;

   watch(
        () => vueRaceId(),
        () => {
            if (vueRaceId() !== 'Effect') return;

            recordStart('Effect');
            state01.value = num + 1;
        },
        { immediate: true }
    );
   watch(
        () => state01.value,
        () => {
            if (!state01.value) return;

            logEffect();
            state02.value = state01.value;
        },
        { immediate: true }
    );
   // 10 まで同じ構造が続きます
   watch(
        () => state10.value,
        () => {
            if (!state10.value) return;

            logEffect();
            recordFinished('Effect');
            num++;
            vueSetRaceId('Props');
        },
        { immediate: true }
    );
</script>
<template>
    <!-- return null -->
</template>

最深部へと最上部への伝播用 Context 設定

<script lang="ts">
   type RelayData = number;
   const initialValue: RelayData = 0;
   type RelayDataRef = Ref<RelayData>;
   const RelayDataSymbol: InjectionKey<RelayDataRef> = Symbol();

   const provideRelayData = (value: RelayDataRef) => {
        provide(RelayDataSymbol, value);
    };

   export const useRelayDataContexts = () => {
        const injected = inject(RelayDataSymbol);
        if (!injected) {
            return {
                relayData: readonly({ value: initialValue }),
                setRelayData: (_: RelayData) => {},
                setRelayDataWithLog: (_: RelayData) => {},
            }
        };

        const relayData = readonly(injected);
        const setRelayData = (value: RelayData) => { injected.value = value; };
        const setRelayDataWithLog = (value: RelayData = 0) => {
            logContext();
            injected.value = value;
        };

        return { relayData, setRelayData, setRelayDataWithLog };
    };
   export type RelayProps = {
        relay: RelayData;
    };
   export default defineComponent({
        name: 'Relay',
        props: {
            relay: {
                type: Number as PropType<RelayData>,
                default: initialValue,
            },
        },
        setup(props) {
            provideRelayData(ref(props.relay));
        }
    });
</script>
<template>
    <slot />
</template>

Parent.vue

<template>
    <RelayContext>
        <Child00 />
    </RelayContext>
</template>

Child00.vue

<script setup lang="ts">
   const { relayData, setRelayData } = useRelayDataContexts();
   const { vueRaceId, vueSetRaceId } = useStore('vueRaceId');
   const relayStart = ref(0);

   watchEffect(() => {
        if (vueRaceId() !== 'Props' || relayData.value) return;

        recordStart('Props');
        relayStart.value = 1;
    }, { flush: 'pre' });

   watchEffect(() => {
        if (vueRaceId() !== 'Context' || !relayData.value) return;

        recordFinished('Context');
        setRelayData(0);
        relayStart.value = 0;

        vueSetRaceId('Memo');
    }, { flush: 'pre' });
</script>
<template>
    <Child01 :relay="relayStart" />
</template>

Child01.vue

<script setup lang="ts">
   const props = defineProps<RelayProps>();

   const relayData = computed(() => {
        if (!props.relay) return 0;

        logProps();

        return props.relay;
    });
</script>
<template>
    <Child02 :relay="relayData" />
</template>

Child10.vue

<script setup lang="ts">
   const props = defineProps<RelayProps>();

   const relayData = computed(() => {
        if (!props.relay) return 0;

        logProps();

        return props.relay;
    });

   const { vueRaceId, vueSetRaceId } = useStore('vueRaceId');
   const { setRelayDataWithLog } = useRelayDataContexts();

   watchEffect(() => {
        if (vueRaceId() !== 'Props' || !relayData.value) return;

        recordFinished('Props');

        vueSetRaceId('Context');
        recordStart('Context');
        setRelayDataWithLog(1);
    }, { flush: 'pre' });
</script>
<template>
    <!-- return null -->
</template>

Memo.vue

<script setup lang="ts">
   const { vueRaceId, vueSetRaceId } = useStore('vueRaceId');

   let num = 0;

   const state01 = computed(() => {
        if (vueRaceId() !== 'Memo') return 0;
        recordStart('Memo');
        return num + 1;
    });
   const state02 = computed(() => {
        if (!state01.value) return;

        logMemo();
        return state01.value;
    });
   // 10 まで同じ構造が続きます
   const finished = computed(() => {
        if (!state10.value) return;

        logMemo();
        recordFinished('Memo');
        return state10.value;
    });
   watch(
        () => finished.value,
        () => {
            if (!finished.value) return;

            num = finished.value;
            vueSetRaceId('Store');
        }
    );
</script>
<template>
    <!-- return null -->
</template>

Store 10 連鎖用グローバルストア設定

type Quantum = number;
const initialValue: Quantum = 0;

export const _vueQuantum = {
    server: {
        vueQuantum01: (): Quantum => initialValue,
        vueSetQuantum01: (_: Quantum) => {},
        // 10 まで同じ構造が続きます
    },
    client: () => {
        const vueQuantum01 = ref<Quantum>(initialValue);
        const vueSetQuantum01 = (value: Quantum) => { vueQuantum01.value = value; };
        // 10 まで同じ構造が続きます

        return {
            vueQuantum01: () => vueQuantum01.value, vueSetQuantum01,
            // 10 まで同じ構造が続きます
        };
    },
};

Quantum00.vue

<script setup lang="ts">
   const { vueRaceId } = useStore('vueRaceId');
   const { vueSetQuantum01 } = useStore('vueQuantum');

   let num = 0;

   watch(
        () => vueRaceId(),
        () => {
            if (vueRaceId() !== 'Store') return;

            recordStart('Store');
            vueSetQuantum01(num + 1);
            num++;
        }
    );
</script>
<template>
    <!-- return null -->
</template>

Quantum01.vue

<script setup lang="ts">
   const { vueQuantum01, vueSetQuantum02 } = useStore('vueQuantum');

   const relayData = computed(() => {
        if (!vueQuantum01()) return 0;

        logStore();

        return vueQuantum01();
    });

   watch(
        () => relayData.value,
        () => {
            if (!relayData.value) return;

            vueSetQuantum02(relayData.value);
        },
        { immediate: true }
    );
</script>
<template>
    <!-- return null -->
</template>

Quantum10.vue

<script setup lang="ts">
   const { vueQuantum10 } = useStore('vueQuantum');
   const { vueSetRaceId } = useStore('vueRaceId');

   const relayData = computed(() => {
        if (!vueQuantum10()) return 0;

        logStore();

        return vueQuantum10();
    });

   watch(
        () => relayData.value,
        () => {
            if (!relayData.value) return;

            recordFinished('Store');
            recordFinished('Race');
            vueSetRaceId(null);
        }
    );
</script>
<template>
    <!-- return null -->
</template>

React 版

グローバルストア用共通設定

機構は他と同じようにしています。

const allStores = {
    ..._raceIdStores,
    ..._quantumStores,
} as const;
export type AllStores = typeof allStores;
export type AllStoreKeys = keyof AllStores;

const createServerStore = (key: AllStoreKeys): AllStores[typeof key]['server'] => allStores[key].server;

type CreateClientStore = ReturnType<AllStores[AllStoreKeys]['client']>;
const createClientStore = (key: AllStoreKeys): CreateClientStore => allStores[key].client();

type ClientStoreInstance = Record<AllStoreKeys, CreateClientStore | null>;
const clientStoreInstance = Object.fromEntries(Object.keys(allStores).map(key => ([ key, null ]))) as ClientStoreInstance;

export const useStore = <K extends AllStoreKeys, R extends ReturnType<AllStores[K]['client']>>(key: K) => {
    if (typeof window === 'undefined') return createServerStore(key) as R;

    if (!clientStoreInstance[key]) {
        clientStoreInstance[key] = createClientStore(key);
    }

    return clientStoreInstance[key] as R;
};

レース種別切り替え用グローバルストア設定

type Store = 'Effect' | 'Props' | 'Context' | 'Memo' | 'Store' | null;
const initialValue: Store = null;
type Listener = () => void;

const serverStore = {
    subscribe(_: Listener) {},
    getServerSnapshot() { return initialValue; },
    getSnapshot() { return initialValue; },
    setStore(_: Store) {},
};
const clientStore = () => {
    let store: Store = initialValue;
    let listeners: Listener[] = [];
    const subscribe = (listener: Listener) => {
        listeners = [...listeners, listener];

        return () => {
            listeners = listeners.filter(l => l !== listener);
        };
    };
    const getServerSnapshot = () => initialValue;
    const getSnapshot = () => store;
    const setStore = (value: Store) => {
        store = value;

        for (let listener of listeners) {
            listener();
        }
    };

    return { subscribe, getServerSnapshot, getSnapshot, setStore };
};
const raceIdStore = { server: serverStore, client: clientStore };
export const _raceIdStores = {
    raceId: raceIdStore,
};

Start.tsx

export default function Start() {
    const raceId = useStore('raceId');

    const handleClick = () => {
        recordStart('Race');
        raceId.setStore('Effect');
    };

    return (
        <fieldset>
            <button onClick={ handleClick }>レーススタート!</button>
        </fieldset>
    )
}

Effect.tsx

export default function Effect() {
    const raceId = useStore('raceId');
    const raceIdStore = useSyncExternalStore(raceId.subscribe, raceId.getSnapshot, raceId.getServerSnapshot);
    const [ state01, setState01 ] = useState(0);
    // 10 まで同じ構造が続きます

    const num = useRef(0);

    useLayoutEffect(() => {
        if (raceIdStore !== 'Effect') return;

        recordStart('Effect');
        setState01(num.current + 1);
    }, [ raceIdStore ]);
    useLayoutEffect(() => {
        if (!state01) return;

        logEffect();
        setState02(state01);
    }, [ state01 ]);
    // 10 まで同じ構造が続きます
    useLayoutEffect(() => {
        if (!state10) return;

        logEffect();
        recordFinished('Effect');
        num.current++;
        raceId.setStore('Props');
    }, [ state10 ]);

    return null;
}

最深部へと最上部への伝播用 Context 設定

export const RelayContext = createContext({
    relayContext: 0,
    setRelayContext: (_: RelayData) => {},
    setRelayDataWithLog: (_: RelayData) => {},
});

Parent.tsx

export default function Parent() {
    const [ relayContext, setRelayContext ] = useState(0);
    const setRelayDataWithLog = (value: typeof relayContext = 0) => {
        logContext();
        setRelayContext(value);
    };

    return (
        <RelayContext value={{ relayContext, setRelayContext, setRelayDataWithLog }}>
            <Child00 />
        </RelayContext>
    )
};

Child00.tsx

export default function Child00() {
    const { relayContext, setRelayContext } = useContext(RelayContext);
    const raceId = useStore('raceId');
    const raceIdStore = useSyncExternalStore(raceId.subscribe, raceId.getSnapshot, raceId.getServerSnapshot);
    const [ relayStart, setRelayStart ] = useState(0);

    useLayoutEffect(() => {
        if (raceIdStore !== 'Props' || relayContext) return;

        recordStart('Props');
        setRelayStart(1);
    }, [ raceIdStore, relayContext ]);

    useLayoutEffect(() => {
        if (raceIdStore !== 'Context' || !relayContext) return;

        recordFinished('Context');
        setRelayContext(0);
        setRelayStart(0);

        raceId.setStore('Memo');
    }, [ raceIdStore, relayContext ]);

    return <Child01 relay={ relayStart } />;
};

Child01.tsx

export default function Child01(props: RelayProps) {
    const relayData = useMemo(() => {
        if (!props.relay) return 0;

        logProps();
        return props.relay;
    }, [ props.relay ]);

    return <Child02 relay={ relayData } />;
};

Child10.tsx

export default function Child10(props: RelayProps) {
    const raceId = useStore('raceId');
    const raceIdStore = useSyncExternalStore(raceId.subscribe, raceId.getSnapshot, raceId.getServerSnapshot);
    const { setRelayDataWithLog } = useContext(RelayContext);

    const relayData = useMemo(() => {
        if (!props.relay) return 0;

        logProps();
        return props.relay;
    }, [ props.relay ]);

    useLayoutEffect(() => {
        if (raceIdStore !== 'Props' || !relayData) return;

        recordFinished('Props');

        raceId.setStore('Context');
        recordStart('Context');
        setRelayDataWithLog(1);
    }, [ raceIdStore, relayData ]);

    return null;
};

Memo.tsx

export default function Memo() {
    const raceId = useStore('raceId');
    const raceIdStore = useSyncExternalStore(raceId.subscribe, raceId.getSnapshot, raceId.getServerSnapshot);

    const num = useRef(0);

    const state01 = useMemo(() => {
        if (raceIdStore !== 'Memo') return 0;

        recordStart('Memo');
        return num.current + 1;
    }, [ raceIdStore ]);
    const state02 = useMemo(() => {
        if (!state01) return;

        logMemo();
        return state01;
    }, [ state01 ]);
    // 10 まで同じ構造が続きます
    const finished = useMemo(() => {
        if (!state10) return;

        logMemo();
        recordFinished('Memo');
        return state10;
    }, [ state10 ]);
    useLayoutEffect(() => {
        if (!finished) return;

        num.current = finished;
        raceId.setStore('Store');
    }, [ finished ]);

    return null;
}

Store 10 連鎖用グローバルストア設定

type Store = number;
const initialValue: Store = 0;
type Listener = () => void;

const serverStore = {
    subscribe(_: Listener) {},
    getServerSnapshot() { return initialValue; },
    getSnapshot() { return initialValue; },
    setStore(_: Store) {},
};
const clientStore = () => {
    let store = initialValue;
    let listeners: Listener[] = [];
    const subscribe = (listener: Listener) => {
        listeners = [...listeners, listener];

        return () => {
            listeners = listeners.filter(l => l !== listener);
        };
    };
    const getServerSnapshot = () => initialValue;
    const getSnapshot = () => store;
    const setStore = (value: Store) => {
        store = value;

        for (let listener of listeners) {
            listener();
        }
    };

    return { subscribe, getServerSnapshot, getSnapshot, setStore };
};
const quantum01Store = { server: serverStore, client: clientStore };
// 10 まで同じ構造が続きます
export const _quantumStores = {
    quantum01: quantum01Store,
    // 10 まで同じ構造が続きます
};

Quantum00.tsx

export default function Quantum00() {
    const raceId = useStore('raceId');
    const quantum01 = useStore('quantum01');
    const raceIdStore = useSyncExternalStore(raceId.subscribe, raceId.getSnapshot, raceId.getServerSnapshot);

    const num = useRef(0);

    useLayoutEffect(() => {
        if (raceIdStore !== 'Store') return;

        recordStart('Store');
        quantum01.setStore(num.current + 1);
        num.current++;
    }, [ raceIdStore ]);

    return null;
}

Quantum01.tsx

export default function Quantum01() {
    const quantum01 = useStore('quantum01');
    const quantum02 = useStore('quantum02');
    const quantumStore = useSyncExternalStore(quantum01.subscribe, quantum01.getSnapshot, quantum01.getServerSnapshot);

    const relayData = useMemo(() => {
        if (!quantumStore) return 0;

        logStore();
        return quantumStore;
    }, [ quantumStore ]);

    useLayoutEffect(() => {
        if (!relayData) return;

        quantum02.setStore(relayData);
    }, [ relayData ]);

    return null;
}

Quantum10.tsx

export default function Quantum10() {
    const quantum10 = useStore('quantum10');
    const quantumStore = useSyncExternalStore(quantum10.subscribe, quantum10.getSnapshot, quantum10.getServerSnapshot);

    const relayData = useMemo(() => {
        if (!quantumStore) return 0;

        logStore();
        return quantumStore;
    }, [ quantumStore ]);

    useLayoutEffect(() => {
        if (!relayData) return;

        recordFinished('Store');
        recordFinished('Race');
    }, [ relayData ]);

    return null;
}

Qwik 版

レース種別切り替え用 Context 設定

export type RaceId = 'Effect' | 'Props' | 'Context' | 'Memo' | 'Store' | null;
export const raceIdData = createContextId<Signal<RaceId>>('raceIdData');

Page.tsx

ページ全体のコンポーネントです(Astro に該当します)

export default component$(() => {
    return (
        <div id="performance">
            <Start />
        </div>
    )
});

Start.tsx

export default component$(() => {
    const raceIdDataSignal = useSignal<RaceId>(null);
    useContextProvider(raceIdData, raceIdDataSignal);

    const handleClick$ = $(() => {
        recordStart('Race');
        raceIdDataSignal.value = 'Effect';
    });

    return (
        <>
            <fieldset>
                <button onClick$={ () => handleClick$() }>レーススタート!</button>
            </fieldset>
            <Effect />
            <Parent />
            <Memo />
            <QuantumCircuit />
        </>
    )
});

Effect.tsx

export default component$(() =>  {
    const raceIdDataContext = useContext(raceIdData);
    const state01 = useSignal(0);
    // 10 まで同じ構造が続きます

    const num = { value: 0 };

    useVisibleTask$(({ track }) => {
        track(() => raceIdDataContext.value);

        if (raceIdDataContext.value !== 'Effect') return;

        recordStart('Effect');
        state01.value = num.value + 1;
    });
    useVisibleTask$(({ track }) => {
        track(() => state01.value);

        if (!state01.value) return;

        logEffect();
        state02.value = state01.value;
    });
    // 10 まで同じ構造が続きます
    useVisibleTask$(({ track }) => {
        track(() => state10.value);

        if (!state10.value) return;

        logEffect();
        recordFinished('Effect');
        num.value++;
        raceIdDataContext.value = 'Props';
    });

    return null;
});

最深部へと最上部への伝播用 Context 設定

export type RelayData = number;
export type RelaySetter = QRL<(_: RelayData) => void>;
export type RelayProps = {
    relay: RelayData;
};
export const relayData = createContextId<Signal<RelayData>>('relayData04');

Parent.tsx

export default component$(() => {
    const relayDataSignal = useSignal<RelayData>(0);
    useContextProvider(relayData, relayDataSignal);

    return <Child00 />;
});

Child00.tsx

export default component$(() => {
    const raceIdDataContext = useContext(raceIdData);
    const relayDataContext = useContext(relayData);
    const relayStart = useSignal(0);

    useVisibleTask$(({ track }) => {
        track(() => raceIdDataContext.value);
        track(() => relayDataContext.value);

        if (raceIdDataContext.value !== 'Props' || relayDataContext.value) return;

        recordStart('Props');
        relayStart.value = 1;
    });

    useVisibleTask$(({ track }) => {
        track(() => raceIdDataContext.value);
        track(() => relayDataContext.value);

        if (raceIdDataContext.value !== 'Context' || !relayDataContext.value) return;

        recordFinished('Context');
        relayDataContext.value = 0;
        relayStart.value = 0;

        raceIdDataContext.value = 'Memo';
    });

    return <Child01 relay={ relayStart.value } />;
});

Child01.tsx

export default component$((props: RelayProps) => {
    const relayData = useComputed$(() => {
        if (!props.relay) return 0;

        logProps();
        return props.relay;
    });

    return <Child02 relay={ relayData.value } />;
});

Child10.tsx

export default component$((props: RelayProps) => {
    const raceIdDataContext = useContext(raceIdData);
    const relayDataContext = useContext(relayData);

    const relay = useComputed$(() => {
        if (!props.relay) return 0;

        logProps();
        return props.relay;
    });

    useVisibleTask$(({ track }) => {
        track(() => raceIdDataContext.value);
        track(() => relay.value);

        if (raceIdDataContext.value !== 'Props' || !relay.value) return;

        recordFinished('Props');

        raceIdDataContext.value = 'Context';
        recordStart('Context');
        logContext();
        relayDataContext.value = 1;
    });

    return null;
});

Memo.tsx

export default component$(() => {
    const raceIdDataContext = useContext(raceIdData);

    const num = { value: 0 };

    const state01 = useComputed$(() => {
        if (raceIdDataContext.value !== 'Memo') return 0;

        recordStart('Memo');
        return num.value + 1;
    });
    const state02 = useComputed$(() => {
        if (!state01.value) return;

        logMemo();
        return state01.value;
    });
    // 10 まで同じ構造が続きます
    const finished = useComputed$(() => {
        if (!state10.value) return;

        logMemo();
        recordFinished('Memo');
        return state10.value;
    });
    useVisibleTask$(({ track }) => {
        track(() => finished.value);

        if (!finished.value) return;

        num.value = finished.value;
        raceIdDataContext.value = 'Store';
    });

    return null;
});

Store 10 連鎖用 Context 設定

前述の通り、Qwik ではグローバルストアを持てないので、Context で代用します。

export default component$(() => {
    const quantum01 = useSignal<Quantum>(0);
    // 10 まで同じ構造が続きます

    useContextProvider(quantumData, {
        quantum01: quantum01,
        // 10 まで同じ構造が続きます
    });

    return (
        <>
            <Quantum00 />
            // 10 まで同じ構造が続きます
        </>
    )
});

Quantum00.tsx

export default component$(() => {
    const raceIdDataContext = useContext(raceIdData);
    const quantumDataContext = useContext(quantumData);

    const num = { value: 0 };

    useVisibleTask$(({ track }) => {
        track(() => raceIdDataContext.value);

        if (raceIdDataContext.value !== 'Store') return;

        recordStart('Store');
        quantumDataContext.quantum01.value = num.value + 1;
        num.value++;
    });

    return null;
});

Quantum01.tsx

export default component$(() => {
    const quantumDataContext = useContext(quantumData);

    const relayData = useComputed$(() => {
        if (!quantumDataContext.quantum01.value) return 0;

        logStore();
        return quantumDataContext.quantum01.value;
    });

    useVisibleTask$(({ track }) => {
        track(() => relayData.value);

        if (!relayData.value) return;

        quantumDataContext.quantum02.value = relayData.value;
    });

    return null;
});

Quantum10.tsx

export default component$(() => {
    const quantumDataContext = useContext(quantumData);

    const relayData = useComputed$(() => {
        if (!quantumDataContext.quantum10.value) return 0;

        logStore();
        return quantumDataContext.quantum10.value;
    });

    useVisibleTask$(({ track }) => {
        track(() => relayData.value);

        if (!relayData.value) return;

        recordFinished('Store');
        recordFinished('Race');
    });

    return null;
});

計測結果

Solid

1 回目

  • Effect: 0.9000000022351742 ミリ秒
  • Props: 0.9000000022351742 ミリ秒
  • Context: 0.09999999776482582 ミリ秒
  • Memo: 0.8000000007450581 ミリ秒
  • Store: 1 ミリ秒
  • Race: 4.300000000745058 ミリ秒

2 回目

  • Effect: 0.7999999970197678 ミリ秒
  • Props: 1.2999999970197678 ミリ秒
  • Context: 0 ミリ秒
  • Memo: 0.8000000007450581 ミリ秒
  • Store: 1.199999999254942 ミリ秒
  • Race: 5.099999997764826 ミリ秒

3 回目

  • Effect: 0.8999999985098839 ミリ秒
  • Props: 1 ミリ秒
  • Context: 0.10000000149011612 ミリ秒
  • Memo: 0.8000000007450581 ミリ秒
  • Store: 1.5 ミリ秒
  • Race: 5.199999999254942 ミリ秒

4 回目

  • Effect: 0.8000000007450581 ミリ秒
  • Props: 1.1000000014901161 ミリ秒
  • Context: 0.10000000149011612 ミリ秒
  • Memo: 0.7999999970197678 ミリ秒
  • Store: 1.1000000014901161 ミリ秒
  • Race: 4.399999998509884 ミリ秒

5 回目

  • Effect: 0.8000000007450581 ミリ秒
  • Props: 1.3999999985098839 ミリ秒
  • Context: 0.09999999776482582 ミリ秒
  • Memo: 0.8999999985098839 ミリ秒
  • Store: 1.1000000014901161 ミリ秒
  • Race: 4.899999998509884 ミリ秒

平均

  • Effect: 0.8399999999 ミリ秒
  • Props: 1.1399999999 ミリ秒
  • Context: 0.0799999997 ミリ秒
  • Memo: 0.8199999996 ミリ秒
  • Store: 1.1800000004 ミリ秒
  • Race: 4.779999999 ミリ秒

Svelte

1 回目

  • Effect: 0.8999999985098839 ミリ秒
  • Props: 1.1399999999 ミリ秒
  • Context: 0.19999999925494194 ミリ秒
  • Memo: 0.8999999985098839 ミリ秒
  • Store: 1 ミリ秒
  • Race: 4.800000000745058 ミリ秒

2 回目

  • Effect: 0.8000000007450581 ミリ秒
  • Props: 0.8999999985098839 ミリ秒
  • Context: 0.10000000149011612 ミリ秒
  • Memo: 0.8000000007450581 ミリ秒
  • Store: 1.1000000014901161 ミリ秒
  • Race: 4.600000001490116 ミリ秒

3 回目

  • Effect: 0.8000000007450581 ミリ秒
  • Props: 0.8000000007450581 ミリ秒
  • Context: 0.10000000149011612 ミリ秒
  • Memo: 0.7999999970197678 ミリ秒
  • Store: 1 ミリ秒
  • Race: 4.300000000745058 ミリ秒

4 回目

  • Effect: 0.5999999977648258 ミリ秒
  • Props: 0.8000000007450581 ミリ秒
  • Context: 0.09999999776482582 ミリ秒
  • Memo: 0.6999999992549419 ミリ秒
  • Store: 1.199999999254942 ミリ秒
  • Race: 4.300000000745058 ミリ秒

5 回目

  • Effect: 0.6999999992549419 ミリ秒
  • Props: 0.6999999992549419 ミリ秒
  • Context: 0.10000000149011612 ミリ秒
  • Memo: 0.6999999992549419 ミリ秒
  • Store: 1.199999999254942 ミリ秒
  • Race: 3.900000002235174 ミリ秒

平均

  • Effect: 0.7599999994 ミリ秒
  • Props: 0.8679999998 ミリ秒
  • Context: 0.1200000003 ミリ秒
  • Memo: 0.779999999 ミリ秒
  • Store: 1.1 ミリ秒
  • Race: 4.3800000012 ミリ秒

Vue

1 回目

  • Effect: 0.6999999992549419 ミリ秒
  • Props: 1.7999999970197678 ミリ秒
  • Context: 0.29999999701976776 ミリ秒
  • Memo: 0.6000000014901161 ミリ秒
  • Store: 0.5999999977648258 ミリ秒
  • Race: 4.800000000745058 ミリ秒

2 回目

  • Effect: 0.9000000022351742 ミリ秒
  • Props: 2 ミリ秒
  • Context: 0.29999999701976776 ミリ秒
  • Memo: 0.6999999992549419 ミリ秒
  • Store: 0.7999999970197678 ミリ秒
  • Race: 5.800000000745058 ミリ秒

3 回目

  • Effect: 0.7000000029802322 ミリ秒
  • Props: 1.9000000022351742 ミリ秒
  • Context: 0.30000000074505806 ミリ秒
  • Memo: 0.6000000014901161 ミリ秒
  • Store: 0.6000000014901161 ミリ秒
  • Race: 4.699999999254942 ミリ秒

4 回目

  • Effect: 0.8000000007450581 ミリ秒
  • Props: 1.9000000022351742 ミリ秒
  • Context: 0.30000000074505806 ミリ秒
  • Memo: 0.5999999977648258 ミリ秒
  • Store: 1 ミリ秒
  • Race: 5.199999999254942 ミリ秒

5 回目

  • Effect: 0.6999999992549419 ミリ秒
  • Props: 1.8999999985098839 ミリ秒
  • Context: 0.19999999925494194 ミリ秒
  • Memo: 0.5 ミリ秒
  • Store: 0.8000000007450581 ミリ秒
  • Race: 4.899999998509884 ミリ秒

平均

  • Effect: 0.7600000009 ミリ秒
  • Props: 1.9 ミリ秒
  • Context: 0.279999999 ミリ秒
  • Memo: 0.6 ミリ秒
  • Store: 0.7599999994 ミリ秒
  • Race: 5.0799999997 ミリ秒

React

1 回目

  • Effect: 2.699999999254942 ミリ秒
  • Props: 1.5 ミリ秒
  • Context: 0.8000000007450581 ミリ秒
  • Memo: 1.300000000745058 ミリ秒
  • Store: 2.599999997764826 ミリ秒
  • Race: 10.699999999254942 ミリ秒

2 回目

  • Effect: 2.800000000745058 ミリ秒
  • Props: 1.4000000022351742 ミリ秒
  • Context: 1.0999999977648258 ミリ秒
  • Memo: 0.8000000007450581 ミリ秒
  • Store: 1.800000000745058 ミリ秒
  • Race: 10.900000002235174 ミリ秒

3 回目

  • Effect: 2 ミリ秒
  • Props: 1.2999999970197678 ミリ秒
  • Context: 0.7999999970197678 ミリ秒
  • Memo: 0.7999999970197678 ミリ秒
  • Store: 2.900000002235174 ミリ秒
  • Race: 9.399999998509884 ミリ秒

4 回目

  • Effect: 2.600000001490116 ミリ秒
  • Props: 1.8999999985098839 ミリ秒
  • Context: 0.8999999985098839 ミリ秒
  • Memo: 0.6999999992549419 ミリ秒
  • Store: 2.300000000745058 ミリ秒
  • Race: 9.699999999254942 ミリ秒

5 回目

  • Effect: 2.800000000745058 ミリ秒
  • Props: 1.6000000014901161 ミリ秒
  • Context: 1 ミリ秒
  • Memo: 0.7000000029802322 ミリ秒
  • Store: 2.599999997764826 ミリ秒
  • Race: 10.5 ミリ秒

平均

  • Effect: 2.5800000004 ミリ秒
  • Props: 1.5399999999 ミリ秒
  • Context: 0.9199999988 ミリ秒
  • Memo: 0.8600000001 ミリ秒
  • Store: 2.4399999999 ミリ秒
  • Race: 10.2399999999 ミリ秒

Qwik

1 回目

  • Effect: 1.5 ミリ秒
  • Props: 0.9000000022351742 ミリ秒
  • Context: 0.20000000298023224 ミリ秒
  • Memo: 1 ミリ秒
  • Store: 1 ミリ秒
  • Race: 5.5 ミリ秒

2 回目

  • Effect: 1.699999999254942 ミリ秒
  • Props: 0.8000000007450581 ミリ秒
  • Context: 0.20000000298023224 ミリ秒
  • Memo: 1 ミリ秒
  • Store: 1 ミリ秒
  • Race: 5.399999998509884 ミリ秒

3 回目

  • Effect: 1.300000000745058 ミリ秒
  • Props: 0.9000000022351742 ミリ秒
  • Context: 0.19999999925494194 ミリ秒
  • Memo: 0.8000000007450581 ミリ秒
  • Store: 0.9000000022351742 ミリ秒
  • Race: 5.100000001490116 ミリ秒

4 回目

  • Effect: 1.5 ミリ秒
  • Props: 0.8999999985098839 ミリ秒
  • Context: 0.10000000149011612 ミリ秒
  • Memo: 0.6999999992549419 ミリ秒
  • Store: 0.8999999985098839 ミリ秒
  • Race: 5.399999998509884 ミリ秒

5 回目

  • Effect: 1.2999999970197678 ミリ秒
  • Props: 0.8999999985098839 ミリ秒
  • Context: 0.30000000074505806 ミリ秒
  • Memo: 0.8000000007450581 ミリ秒
  • Store: 0.7999999970197678 ミリ秒
  • Race: 5.199999999254942 ミリ秒

平均

  • Effect: 1.4599999994 ミリ秒
  • Props: 0.8800000004 ミリ秒
  • Context: 0.2000000015 ミリ秒
  • Memo: 0.8600000001 ミリ秒
  • Store: 0.9199999996 ミリ秒
  • Race: 5.3199999996 ミリ秒

平均値ランキング

Effect

  1. Svelte: 0.7599999994 ミリ秒
  2. Vue: 0.7600000009 ミリ秒
  3. Solid: 0.8399999999 ミリ秒
  4. Qwik: 1.4599999994 ミリ秒
  5. React: 2.5800000004 ミリ秒

Props

  1. Svelte: 0.8679999998 ミリ秒
  2. Qwik: 0.8800000004 ミリ秒
  3. Solid: 1.1399999999 ミリ秒
  4. React: 1.5399999999 ミリ秒
  5. Vue: 1.9 ミリ秒

Context

  1. Solid: 0.0799999997 ミリ秒
  2. Svelte: 0.1200000003 ミリ秒
  3. Qwik: 0.2000000015 ミリ秒
  4. Vue: 0.279999999 ミリ秒
  5. React: 0.9199999988 ミリ秒

Memo

  1. Vue: 0.6 ミリ秒
  2. Svelte: 0.779999999 ミリ秒
  3. Solid: 0.8199999996 ミリ秒
  4. React / Qwik: 0.8600000001 ミリ秒

Store

  1. Vue: 0.7599999994 ミリ秒
  2. Qwik: 0.9199999996 ミリ秒
  3. Svelte: 1.1 ミリ秒
  4. Solid: 1.1800000004 ミリ秒
  5. React: 2.4399999999 ミリ秒

Race

  1. Svelte: 4.3800000012 ミリ秒
  2. Solid: 4.779999999 ミリ秒
  3. Vue: 5.0799999997 ミリ秒
  4. Qwik: 5.3199999996 ミリ秒
  5. React: 10.2399999999 ミリ秒

番外編は、いずれも 5 回ずつの結果とは言え、有意な数値とは言えませんが、この結果になりました。 Svelte のコンパイルの超強力さと、Vue が実際の構築でよく使う Effect / Memo / Store それぞれにおいてかなりの好成績であることが印象的でした。 今回も、とても面白い実験でした。この一連の計測実験は、今回で閉幕とします。お読みいただきありがとうございました。