要約
SolidJSは軽量でパフォーマンスも良い!
その秘訣は「仮想DOM」を使わないこと!
実DOM更新に考慮したコンポーネントも提供してるよ!
はじめに
皆さん、SolidJSというライブラリをご存知でしょうか。
最近結構名前を聞くようになりましたよね。Reactっぽいけど、Reactと比べてパフォーマンスがめちゃくちゃいいんだぞ!みたいな。
じゃあ実際、Reactと比べて何が違うん?といった疑問もあるかと思います。
今回の記事では、そんな疑問を解決するために、SolidJSのパフォーマンスの秘密に迫っていきます。
SolidJSの紹介
公式サイト
GitHub
Star数:24K (2022/12/12時点)
SolidJSは、Reactと同様に「UIを作成するためのJavaScriptライブラリ」です。
大きな特徴として、パフォーマンスやシンプルさを重視して開発されています。
SolidJSがReactと最も違う点
SolidJSがReactと最も異なる部分、それは...
仮想DOMを使っていない
という点です。
仮想DOMってなんぞ
仮想DOMは、実際のDOMとは別物として、仮想のDOMを表現する概念です。
仮想DOM自体はあくまで概念で、それ自体の実装はそれぞれのUIライブラリによって異なりますが、例えばReactであれば、メモリ上に仮想のDOMツリーのオブジェクトを構築し、それを専用のアルゴリズムを使って実DOMと比較することで、高パフォーマンスでDOMの再構築を行えるようになる、といった感じです。
仮想DOMを使うことによるメリット
仮想DOMを使うことによるメリットとして、
宣言的UIを実現可能にする
という点が挙げられます。
これについて、過去に備忘録を作成しているので、よければこちらの記事をご覧ください。
要約すると
- 宣言的UIを実現するために仮想DOMが必要だった
- 理由は、DOMの変更部分を特定するアルゴリズムの計算量が大きいこと
- 仮想DOMと実DOMを比較して、変更を特定し実際のDOMに反映することで、現実的なパフォーマンスを維持しつつ、宣言的UIを実現可能にした。
といった感じです。
そもそも仮想DOMがハチャメチャに早いというわけではなく、DOMの更新を最小限に抑えるためのサポートをしてくれる、という認識の方が良さそうです。
仮想DOMのデメリット
それをデメリットと言ったらキリが無いんじゃ...みたいな部分だとは思いますが、
DOMへ変更を反映するために一定量のコストが必要
という点が仮想DOMのデメリットぽいです。
あとは、仮想DOMを使うためのライブラリ自体のサイズが多少大きいことでしょうか。
仮想DOMを使ったDOM更新の方法が早いのは間違いないのですが...
結局のところ一番早いのは、バニラのJavaScriptやjQueryのように直接DOMのある部分を指定して命令的に変更を反映することです。それと比べてしまうと、どうしてもパフォーマンスは劣ってしまいます。
SolidJSはどうなん?
ここまで見た感じ、宣言的UIの実現には仮想DOMが必須っぽいが、一定量の変更コストには目を瞑らなくてはいけない、といった感じでした。
ではSolidJSはどうなのかというと、この子は仮想DOM無しで宣言的UIを実現しているらしいです。すごい。
てわけで、SolidJSがどのようにしてそれを実現しているのか見ていきます。
SolidJSには仮想DOMが必要ない
結論としては、SolidJSは
宣言的なコードを、DOMを直接制御する命令的なコードにコンパイルして実行しているため、バニラに匹敵するパフォーマンスを出せる
みたいです。
そのため、「SolidJSには仮想DOMが必要ない、むしろ仮想DOMが無いこと自体がSolidJSの強みとなっている」という認識です。
コンパイル例
コンパイル前
SolidJSはJSXに対応しており、基本的にはJSXで開発を行うことになると思います。
例えば以下のコードです。
異なる点はあるのですが、Reactで書くようなコードとほぼほぼ同じに見えます。
import { render } from "solid-js/web";
import { createSignal } from "solid-js";
function Counter() {
const [count, setCount] = createSignal(1);
const increment = () => setCount(count() + 1);
return (
<div>
<button type="button" onClick={increment}>
{count()}
</button>
{count() > 8 && <div>count is more than 8</div>}
</div>
);
}
render(() => <Counter />, document.getElementById("app")!);
コンパイル後
コンパイルなので当然といえば当然ですが、かなり変わりましたね。
import { template as _$template } from "solid-js/web";
import { delegateEvents as _$delegateEvents } from "solid-js/web";
import { createComponent as _$createComponent } from "solid-js/web";
import { memo as _$memo } from "solid-js/web";
import { insert as _$insert } from "solid-js/web";
const _tmpl$ = /*#__PURE__*/ _$template(
`<div><button type="button"></button></div>`,
4
),
_tmpl$2 = /*#__PURE__*/ _$template(`<div>count is more than 8</div>`, 2);
import { render } from "solid-js/web";
import { createSignal } from "solid-js";
function Counter() {
const [count, setCount] = createSignal(1);
const increment = () => setCount(count() + 1);
return (() => {
const _el$ = _tmpl$.cloneNode(true),
_el$2 = _el$.firstChild;
_el$2.$$click = increment;
_$insert(_el$2, count);
_$insert(
_el$,
(() => {
const _c$ = _$memo(() => count() > 8);
return () => _c$() && _tmpl$2.cloneNode(true);
})(),
null
);
return _el$;
})();
}
render(() => _$createComponent(Counter, {}), document.getElementById("app"));
_$delegateEvents(["click"]);
自分が理解している範囲での話になってしまいますが、重要なのは以下の点だと思っています。
- 変更の必要がないようなHTMLコードについてテンプレート化している
- 作成したテンプレートからcloneNodeして作成したものを実際のDOMに反映している
- リアクティブな値を参照しているような変更が必要な可能性のある処理については、実際に変更を加えられた際に反映が行えるようにしている
(まだこの辺の理解が曖昧なので、もう少し深掘りしたいな)
使用しているコンパイラ
SolidJSのコンパイラには、同じ作者が制作されている以下のパッケージが使われています。
色々見てみたのですが、Ryan氏の思想として「モダンなUIライブラリから、仮想DOMよって仕方なく発生するコストを無くしたい」というものがあり、このコンパイラもSolid専用ではなく汎用的に使えるようにされているっぽいです。
(誰か試してみてほしいなぁ〜。)
独自の構文用コンポーネント
補足程度になるのですが、Solidのパフォーマンスの良さにはもう一つ秘密があります。
それが、Solidから提供される独自の制御フローコンポーネントです。
いくつかありますが、軽くForコンポーネントを見てみましょうか。
Forと書いてはいますが、使い方的にはmapメソッドと同様だと思っていただいた方が良いです。
まず、通常通りmapメソッドで繰り返しを行う場合です。
const [cats, setCats] = createSignal([
...
]);
return (
<ul>
{cats().map((cat, i) => (
<li>
{i + 1}: {cat.name}
</li>
))}
</ul>
);
見慣れた形ですね。(SolidJSでは、Reactで言うところのStateがゲッター関数になっていることだけ気になりますが。。。)
続いて、SolidJSが提供するForコンポーネントの例です。
const [cats, setCats] = createSignal([
...
]);
return (
<ul>
<For each={cats()}>
{(cat, i) => (
<li>
{i() + 1}: {cat.name}
</li>
)}
</For>
</ul>
);
このようになります。慣れないうちは若干困惑しそうではありますが。。
提供される構文コンポーネントを使うメリットとして、公式は以下の2点を挙げています。
- 可読性の向上
- DOMレンダリングパフォーマンスの向上
ここで紹介したForコンポーネントには、配列が変更された際にDOMを再生成せず、配列の要素を入れ替えたり更新できる特殊効果がついてきます。すげー。
提供される構文コンポーネントについては必ずしも使う必要はなく、「書き方が気に入らねぇ〜〜〜〜」って人は使わなくてもおっけーです。ただ、最大限Solidのメリットを引き出すのであれば、積極的に使っていくことを推奨します。
(ちなみに自分はあまり好きじゃないです。。。普通にmapメソッド使いたみ)
他コンポーネントについてはこちらに記載があるので、興味のある方はぜひご覧ください。
おわりに
まだまだ秘密はあると思いますが、ひとまずSolidJSのパフォーマンスの良さについて理解を深められたのでよかったです🙌
今回の記事で調べた点はあくまで「DOM更新のパフォーマンス」という点なので、ひょっとしたら自分の調べが足りていない部分もあるかも知れません。
(もし補足したいことや間違いがあればコメントください。ぼくが喜びます。)
フロントエンドって技術の移り変わりが激しい印象ですし、今後SolidJSのように仮想DOMを使わないUIライブラリが増え、それがデファクトになることもあり得るかもですね。
とはいえ、ReactやVueには年月をかけて育てられてきた大きなエコシステムの存在がありますし、しばらくはReactやVueが使われるのかなぁ、という気持ちです。
SolidJSはまさに今発展途上の段階にあり、今後フレームワーク等の開発によるエコシステム成長が期待されています。ぼくも期待してます。
なので、この記事を最後まで見てくれたそこのあなた、是非SolidJS用のライブラリを作ってくだせぇ。よろしくお願いします。(他人事)
参考記事
宣伝
再び宣伝しちゃいます。
仮想DOMのメリットってなんだっけというのを別記事でまとめてます。
よければこちらの記事もご覧ください。