Reactに有利なベンチマークを Vue.js で試したところ大差なく、そして…

2022/07/14に公開約6,300字2件のコメント

みなさんこんにちは。
現在、フロントエンドでは宣言的UIが大流行しており、そのためのフレームワークも Vue.js をはじめ複数存在しています。
(React はフレームワークではなくライブラリです)

同種のソフトウェアや言語があれば、自分の好みを巡って意見を出し合うのはエンジニアの常でして。
それがパフォーマンスに関することであれば、無関心ではいられなかったりするわけです。

とはいえ Evan You もいうように特定のフレームワークやライブラリが現実世界のパフォーマンスの問題を銀の弾丸のように解決できるわけではありません。
フレームワークの開発者が数10ミリ秒単位でパフォーマンス改善に勤しむなか、利用する企業が(数100ミリ秒要するような)広告会社のスクリプトを迷いなく追加したりするのですから。

それでも僕たちは、パフォーマンスの話題をせずにはいられません。
だって、それがエンジニアですから…

元記事「Reactに有利なベンチマークを作ってみた」

https://qiita.com/uhyo/items/35cb243557df5e1a87fc

最近『WEB+DB PRESS Vol.129』の「Reactの深層」や『プロを目指す人のためのTypeScript入門』を読みましたが、この元記事も同じく @uhyo_ さんが書いています。
ほとんどのひとが React や TypeScript 関連で何かしら参考にしたことがあるでしょう。
もちろん僕もそのひとり。

普段 Nuxt 3 や Vue.js 3 を使っていますが、React の思想は大好きなので、ドキュメントや記事はどれも参考にしています。

さて、記事によると React は「もっとも真剣にパフォーマンスに取り組んでいるUIライブラリ」であり、ベンチマークもそれを活かす内容になっているようです。
ベンチマークの基準は「入力の快適さ」ということで、実際の Web アプリケーションでは重要視される項目ですね。
フロントエンドの責務が広がっているなか(また、リアクティブなUIを実現するための処理コストを鑑みても)、ユーザーに負担をかけてはならない要素のひとつが input かもしれません。

その意味で、今回のこのベンチマーク用アプリケーションは、とてもリアルワールドに即した内容だと思います。

Vue.js のパフォーマンスが……

詳しいベンチマークの内容は当該記事をご覧いただきたいのですが、React の 513.6ms に対して Vue.js は 1,179ms と2倍近い数字になってしまっていました。
ソースコードを見ても、さほど難しいことをしていないため、(最適化されているほどではなくても)パフォーマンスの差に大きく影響を及ぼすようなことはなさそうな気がしました。
(実際に HTML クラスのバインディングを微修正したり、v-memo を使ったりしても結果は大きく変わることはありませんでしたし、コンポーネントを細分化すれば当然その分のオーバーヘッドを生みます)

Svelte や SolidJS といった Vue.js と同様にリアクティブな値の存在する要素のみを更新するフレームワーク(ライブラリ)も React ほどのパフォーマンスは得られないという結果。
分析として、Reactが力を入れているジョブスケジューリングの効果により、input 要素の入力を優先度の高いステート更新として扱っているためではないかと書かれていました。
Vue.js も現時点では React と同様、仮想DOMを採用していますが、ステート更新の優先度によって描画の順序を変えているという話を聞いたことはありません。

前置きが長くなりましたが、せっかくソースコードが公開されているので、実際に自分でも動かしてみることにしました。

手元の環境では React と Vue.js の差は小さかった

リポジトリをクローンしたところ、複数の環境が monorepo で提供されていて、最上位に index.html がある親切設計でした。
計測用に bench.js も用意されています。
(といっても使い方が分からず、コンソールで直接実行しました)

benchmark() を実行すると、コンソールに101回の実行結果と中央値が表示されます。
MacBook Pro (15-inch, 2019) で実施したところ

負荷なし 1回目 2回目
React 696.9 ms 717.2 ms
Vue.js 719.0 ms 731.9 ms

Dev Tools のパフォーマンスより 4x slowdown にし実行したところ

負荷あり 1回目 2回目
React 2,432 ms 2,399 ms
Vue.js 2,899 ms 2,820 ms

という結果。

差はついていますが、記事のようなダブルスコアという感じではありません。

画面描画の違い

負荷をかけると、画面描画の違いが際立ちます。
Vue.js が都度ハイライト表示部を更新しているのに対し、React は入力値「ball」が入力されるまで更新されません。

また、React はデフォルトでは影響のあるすべてのコンポーネントを再描画するため、Vue.js と違い、すべてのコンポーネントが更新されている様子もわかります。
(このベンチマークアプリが、入力値がない状態と、入力値がありヒットしていない状態を分けているため、入力値のリセット時にその違いが明確になっています)

React

Reactの動作

Vue.js

Vue.jsの動作

Playwright でやってみた

負荷ありで実施していると、とてもパソコンがもっさりしてきて辛いので、Playwright で試すことにしました。

試行回数や文字列の入力スピードを変更すると、結果がどのように変化するかを見たかったのです。

ブラウザは Chromium のみとし、build 後の React および Vue.js のふたつのみを対象に、同じ文字列 (ball) を { delay: 100 } で 100 回入力するように設定しました。

React: 108,533 ms
Vue.js: 98,479 ms
(100回試行の前後の Date.now() の差分を比較)

Vue.js のほうが速いという結果に。

仮説としては、入力後にハイライト表示部分のテキストの中身をチェックしているので、Vue.js のほうがほんのわずかにハイライト完了が早いのではないかと思います。
(Playwright で実施してもハイライトは ball の入力後だったので)

それでも遅いのでチューニングしてみた

とはいえ、入力時にもたつくというのは変わらないので、しっかりとチューニングしてみようと思います。

Composables に書き換えて、ロジックを分離する

コードの修正をしやすいように Composables に書き換えます。

Composables は Composition Function とも呼ばれる React Hooks 相当の実装です。

ある程度予想していましたが、ファイルを分割していく分さらに遅延することになりました。
(概ね 2,000ms 前後まで増えました。負荷なしでも)

その後いろいろ試しましたが、もっとも効果的だったのは、computed で処理されている箇所を watch を使い値の更新時だけ必要な画面描画がされるようにリアクティブな値の変更が行われるようにしたことです。

これにより 1,500ms くらいまでになりました。
しかし、本質的な改善とはいえないため、React が速い理由である、input 要素とその他の画面描画を分離することにします。

VueUse の refThrottled を使用する

Vue.js には Composables を使用したユーティリティのコレクション VueUse があります。

このなかに refThrottled という function があり、次のように利用することができます。

import { refThrottled } from '@vueuse/core'

const input = ref('')
const throttled = refThrottled(input, 1000)

上記の例では <input v-model="input"> のようにするだけで throttled を使って他の要素の画面描画を少なくとも1秒間隔にすることができます。

試しに検索ワードの入力速度を踏まえて 100ms ほど遅延させたところ、さくさく動くようになりました。
(中央値で 503ms です)

Vue.jsの動作

まとめ

冒頭に記載したように、ライブラリやフレームワークの速度比較をどれだけしても、現実世界のパフォーマンスの差を表すには不十分になっています。

個人的には今回 React がどのような視点でパフォーマンスを最適化しているかを知ることができてよかったです。

Vue.js は React だけではなく Svelte や SolidJS 等を参考に、新しい機能追加をしています(もしくは予定しています)

今後も当面フロントエンドの技術を中心に Web アプリケーションは進化していくことでしょう。
個人的には Vue.js での開発がいちばん好きですが、視野を広く持ち、ほかのよいところ・よい実装も参考にしながら、利用者の体験が最適となるように開発していけたらと思います。

ライブラリもフレームワークも、思想があってそれに伴った良さがあります。
適材適所で活用していこうと思います。

速さだけにとらわれないようにしたいですね。

でも Vite は速いから使うよね

はい、速さと軽さは正義です。
だって、エンジニアだもの…

(おまけのおまけ)ハードモードも同じ対応で難なくクリアできました

元記事に追記があり、新たな記事を追加したとこのこと。

https://qiita.com/uhyo/items/4cef029d140923f205d4

「ハードモード」と銘打って、より、React のスケジューリングの優位性が際立つベンチマークにしたとのこと。
これは挑戦せずにはいられません。

今回の計測値は次の2点

inputDelay: 負荷がない状況に比べてどれくらい入力に時間がかかったかを示します。スケジューリングがダイレクトに効く種目です。
renderingOverhead: キー入力開始から最終的なレンダリング結果が表示されるまでにかかった時間から、キー入力にかかった時間(64ms × 3)を引いた時間です。スケジューリングだけでなくレンダリングの自体の速さも含めた総合力が問われる種目です。

新たな縛りとして「レンダリング完了までメインスレッドをアイドル状態にしない」が加わりました。

4文字目を入力してから最初に requestIdleCallbackのコールバックが発火した時点ですでにDOMに4文字目に対するレンダリング結果が反映されている必要

があるそうです。

チューニング前

手元の環境では次のような結果になりました。
(時間がかかるので試行回数を5回で実施しています)

負荷なし inputDelayList renderingOverheadList
React 4,545 ms 7,685.4 ms
Vue.js 5,709 ms 6,802.9 ms

元記事とはやはり違う結果となりました。
あれですね、当方のマシンスペックが劣っているのでしょう…

チューニング後

環境ごとに差が出そうですが、今回もいろいろ試してみました。
結果的に、前回と同様 refThrottled による間引きがもっとも効果的で、かつ今回の制限事項にも引っかかりませんでした。

今回は必要最低限の対応のみを行ったコミットオブジェクトを作っておきました。

https://github.com/monsat/ui-library-benchmark/commit/985cab5874a3c2ccdb0efb022ac6b478d2ef2eb3

結果は次の通り(Vue.js のみ更新しています)

負荷なし inputDelayList renderingOverheadList
React 4,545 ms 7,685.4 ms
Vue.js 4,761 ms 5,954.8 ms

inputDelayList は時間がかかってますが renderingOverheadList は圧勝です。

手元の環境では 22ms だけ遅延させるとこのような結果になりました。

今回も React はインクリメンタルなハイライト表示は行われませんでした。
これ、同じ条件で、1文字ずつハイライトさせることってできるのですかね🤔

1文字ずつハイライトしないならEnterキーで発火させるほうが良さそうな気がします。

たとえば、マッチした要素のみを絞り込んで表示するように変更してみましたが、やはり(手元の環境では)ball と打ったあとに絞り込まれました。
そうなると違いは顕著です。

というわけでハードモード編でした。
普段あまりこんなにたくさんのコンポーネントを使用する機会がないので、動きが重くなってきたとき Vue.js や VueUse 等のエコシステムをどのように活用するかがわかってよかったです。

Discussion

Vue.jsが遅いってのはよく聞きます。
でも、実際には性能が変わらず、更には、やり方によってはVue.jsが性能がいいのは吃驚しました!

遅いって話を耳にすることもありますね。
まあライブラリやフレームワーク単体で考えるより、アプリケーションとしてのパフォーマンスに注目することは大切ですねー

ログインするとコメントできます