パフォーマンスなんか気にしたくない
Give a man a bug and he'll work for a day.
Give a man a benchmark and he'll work for a lifetime.1
https://twitter.com/awesomekling/status/1318615899910307850
パフォーマンスなんかに気をとられながら実装したくないんですよ。
React の memo()
や useCallback()
のような最適化のためだけの API を呼ぶ呼ばないで 1 ミリ秒も悩みたくないんです。そんな API は存在しないでほしい。
でも気になっちゃうんです。というか、まったく最適化せずに React でアプリを構築していくと、カクつきを体感するぐらいには遅くなりますよね。気にせざるを得ません。
つい最近も React のパフォーマンスチューニング記事がバズってましたね。
- Reactのパフォーマンスチューニングの歴史をまとめてみた
https://blog.ojisan.io/react-re-render-history - React / Redux を実務で使うということは
https://zenn.dev/suzuesa/articles/35ace7a7cd127f9a1d08
早すぎる最適化は害悪か
早すぎる最適化は諸悪の根源とまで言われています。とても強烈な言葉です。本当にそうでしょうか? React でもそうでしょうか?
memo()
をガチで導入しようと思ったら、 MobX のようなリアクティブなグローバルデータストアを各コンポーネントから直接参照するか、あるいは状態ツリーを不変にするか(そのために Immutable.js や Immer を導入しようか)、みたいな話になるかと思います2。 **実装が進んでから最適化しようと思ったら、状態の持ち方から変えることになるかもしれません。**これはさすがにつらい。
パフォーマンス問題は得てして発見が遅れがちです。普段の開発用 PC ではサクサク動くのに、 2 年前に出たローエンドスマホでテストしてみたら全然動かない、みたいな形で姿を現します。手元では再現しません。ローエンドスマホでのデバッグで遅い原因を突き止めて最適化するのはなかなかの試練です。
そして 1 度でもパフォーマンスが課題になるとソースコードのあっちもこっちも気になって気になってムズムズしてきます。最適化はどれだけ時間があっても足りません。(ムズムズしても、極端に遅い箇所だけの最適化にとどめて他の新機能開発に時間を使うのがオトナというもの。最適化に時間を費やしすぎることこそが害悪なのです。...なかなかオトナにはなれないものですけどね...)
最初からなるべくパフォーマンスが問題にならないような仕組みを導入しておくのが無難なのです。
早すぎる最適化はリスク回避です。 少なくともこの文脈では。
可能なら意識しなくても十分にパフォーマンスが出るようなライブラリを選びたいところですよね。
ということで、 React に似た API と JSX で、勝手に爆速になる UI ライブラリ Solid を紹介します。
Solid
- ryansolid/solid: A declarative, efficient, and flexible JavaScript library for building user interfaces.
https://github.com/ryansolid/solid
Solid is a declarative JavaScript library for creating user interfaces. It does not use a Virtual DOM. Instead it opts to compile its templates down to real DOM nodes and wrap updates in fine grained reactions. This way when your state updates only the code that depends on it runs.
[DeepL 翻訳]
Solid は、ユーザー インターフェイスを作成するための宣言型 JavaScript ライブラリです。仮想 DOM は使用しません。その代わりに、テンプレートを実際の DOM ノードまでコンパイルして、更新をきめ細かいリアクションでラップします。これにより、状態が更新されたときに、それに依存するコードのみが実行されます。
Solid は、
React のように JSX とフックライクな API で宣言的な関数コンポーネントを記述でき、
lit-html のように効率的にテンプレートから DOM を構築・更新し、
Svelte のようにコンパイル時に再レンダリングを最適化し、
SSR もサポートする、軽量で非常に高速な UI ライブラリです。
公式コード例(TSX)
import { createState, onCleanup, Component } from "solid-js";
import { render } from "solid-js/dom";
const App: Component = () => {
const [state, setState] = createState({ count: 0 }),
timer = setInterval(() => setState("count", c => c + 1), 1000);
onCleanup(() => clearInterval(timer));
return <div>{state.count}</div>;
};
render(() => <App />, document.getElementById("app"));
なんとなく読めるのではないでしょうか。
React と特に大きく異なるのは、関数コンポーネント内の処理が要素初期化時に 1 度だけ呼ばる点です。 React のようにレンダリングのたびに呼ばれるわけではありません。ここは挙動の違いを明確に意識する必要があると思います。
React の memo()
や useCallback()
のような最適化用の API はありません。それでいて React や Vue.js (3.0) はもちろん、 lit-html や Svelte よりも高速に動作します。ベンチマークを見ると vanillajs にかなり近いです。
- Official results for js web frameworks benchmark
https://krausest.github.io/js-framework-benchmark/index.html
TSX で書けて型チェックできるし、 API もわりと扱いやすいし、この実行速度。
(私の観測範囲では) 控えめに言って最強です (2020 年 10 月現在)。
注意点
どんなライブラリにもクセはあります。私は React の onChange
が "input"
イベントで発火するのが許せない人です。私は Mr. Complain です3。 Angular のこともいろいろ書きました。 まだぜんぜん使い込んでいませんが Solid に対しても不満はあります。
state や props をデストラクチャリングしちゃダメ
createState()
で作成する状態やコンポーネントの引数 props はフィールドアクセスが変更監視のトリガーになっていて、リアクティブにしたい式の中でアクセスする必要があります。デストラクチャリングすると、プロパティの変更に対してリアクティブに動作しなくなってしまいます。
- Answering Common Questions about SolidJS - DEV # 4. Why does destructuring not work? I realized I can fix it by wrapping my whole component in a function.
https://dev.to/ryansolid/answering-common-questions-about-solidjs-23ea#4-why-does-destructuring-not-work-i-realized-i-can-fix-it-by-wrapping-my-whole-component-in-a-function
変更監視系 API をコンポーネントの外で使うと警告
Solid では createSignal()
API によってリアクティブでアトミックな状態、 createState()
API によってリアクティブな状態ツリーを作ります。これらは React の useState()
と異なり、グローバルスコープで普通に作ればコンポーネント間で状態を共有できます。(フックと違ってコンポーネント内で呼ぶ順番も関係ありません。)非常に便利です。
- Reactivity (solid/reactivity.md at master · ryansolid/solid)
https://github.com/ryansolid/solid/blob/master/documentation/reactivity.md
これら状態を監視して変更に反応する、たとえば createMemo()
(MobX でいう computed()
、 Vue.js でいう computed()
、 Recoil でいう selector()
)などの API もグローバルスコープで期待通りに動作します。が、警告が出ます。
computations created outside a
createRoot
orrender
will never be disposed
この警告の意図は理解できます。が、現実のユースケースに照らして考えると、この警告は親切すぎるというか、大きなお世話だと感じます。
Solid の作者によれば、グローバルな状態には Context API がオススメとのことですが、めちゃくちゃ単純で直感的な createSignal()
+ createMemo()
に比べると Context API は複雑で扱いづらいです。
グローバルな状態を作る方法として、変更監視 API の実際の呼び出しをルートコンポーネントの初期化まで遅延させる手は使えそうです。
https://codesandbox.io/s/lmrb9?file=/index.tsx
ためしに作ってみたもの
- Todo リスト
https://codesandbox.io/s/solidjs-todo-nsgru?file=/index.tsx - ライフゲーム
https://luncheon.github.io/conway-game-of-life--react-vs-preact-vs-solid/
ライフゲームの例では React + Recoil、 Preact + preact-shared-state-hook、 Vue.js 3.0、 Solid でライフゲームロジックを共有して FPS を比較してみました。 Solid はバンドルサイズが最小になり、かつ、 FPS も他と比べると群を抜いて良い結果になりました(ベンチマークに適した例ではないかもしれませんが)。(参考に手元の PC で React 12 fps、 Preact 12 fps、 Vue.js 8 fps、 Solid 28 fps 程度です。)
おわりに
Solid は React に近い API と Vanilla JS に近い実行速度を持つ優れた UI ライブラリです。パフォーマンスのことばかりに気をとられる日々は終わりを迎えるかもしれません。
とはいえ、まだ歴史の浅いライブラリですのでエコシステムが未成熟です。コンポーネントフレームワーク(Material-UI とか Element UI みたいなの)は見当たらないし、ネット上で見つかる事例も少ないです。日本語の記事ぜんぜんないし。いま使うなら多少の苦労は覚悟しなきゃいけないかもしれません。
私はその苦労の価値があるんじゃないかと踏み、 Solid に期待を込めて、この記事を書いている次第です。
参考
- Why SolidJS: Do we need another JS UI Library? - DEV
https://dev.to/ryansolid/why-solidjs-do-we-need-another-js-ui-library-1mdc - Comparison with other Libraries (solid/comparison.md at master · ryansolid/solid)
https://github.com/ryansolid/solid/blob/master/documentation/comparison.md