12周年記念パーティ開催! 2024/5/10(金) 19:00

SolidJS入門 第1回 他のライブラリとの違いとコード例から見る特徴

JavaScriptの標準機能を活かし、ブラウザ上で高速に動作することを目指して作られた、SolidJSを解説します。まずはその特徴をつかんでみましょう。

発行

著者 渡辺 由 フロントエンド・エンジニア
SolidJS入門 シリーズの記事一覧

はじめに

SolidJSは、リアクティブなWeb UIを構築するためのJavaScriptライブラリです。2021年6月にバージョン1.0がリリースされ、それと前後して注目度も増してきているようです。「State of JS」いう世界規模のJavaScriptユーザーのアンケート調査の2021年版では、「満足度」で1位、「興味」では2位になっています(フロントエンドフレームワーク ランキング)。反面、「利用率」「認知度」はそれほど高くありません。

「State of JS」の「フロントエンドフレームワーク ランキング」より

リアクティブ、つまりユーザーの操作やデータの変更などに応じて、動的に変化するような画面を作るためのライブラリですから、少々乱暴に言ってしまえば、このランキングの「利用率」のトップのReactや、「興味」でトップのSvelteなどと同じような役割のツールということになります。

ですが、SolidJSは実行時のパフォーマンスを追求したり、他のライブラリの機能を取り入れて再構築するなど、ほかにはない特色を持っています。このシリーズをとおして、その特色や、実際にプロダクトで使用する際のイメージを掴んでもらえたらと考えています。

SolidJSの概要

コンセプトと特徴

公式サイトにアクセスすると、まず「パフォーマンス」「シンプル」という単語が登場します。SolidJSは、JavaScriptの標準機能を活かす、レンダリングの粒度・タイミングを最適化するなどして、ブラウザ上で高速に動作します。さらにはReactKnockoutを特に参考にしていることも書かれていますが、異なる実装方法を用い、より使いやすいAPIを目指しています。

また、React同様にJSX記法でコンポーネントを記述できますが、実装としてはVirtualDOMを採用しておらず、この点もパフォーマンスが良いという特徴につながっています。

他のUIライブラリとの違い

続いて、他のUIライブラリとの比較についても少しご紹介します。

まずはパフォーマンスについてです。公式サイトのトップページにもベンチマークのグラフが掲載されていますが、これは手前味噌な統計ではなく、js-framework-benchmarkというプロジェクトによるものです。これによると、DOM更新の処理速度がもっとも速いフレームワークがSolidJSです。SvelteやPreactよりも速く、さすがにVanilla JS(ピュアなJavaScript)には敵いませんが、それにかなり近づいています。

グラフはSolidJS公式サイトより

続いて、ライブラリのサイズです。SolidJSの作者であるRyan Carniato氏自らが、TodoMVCを作った際のファイルサイズの比較を記事にしています。これによると、ライブラリ本体とToDoコンポーネントのコードを合わせたサイズは、SvelteやPreactには敵わない部分もありますが、ReactやVueとの比較ではライブラリ本体のコードが非常に小さいことがわかります。

表はRyan Carniato氏の記事より。コードはミニファイしているとのこと。

総合すると、SolidJSの特徴は、コンポーネント指向やJSXなど馴染みのある方法が使えて、パフォーマンスが良く、コードサイズも膨れ上がらない、といったところでしょうか。

現在の開発フェーズ

執筆時点でのバージョンは1.4.6です。1.0のリリース以降は大きな仕様変更は減ってきており、代わりに機能の追加やブラッシュアップが行われている印象です。チュートリアルやサンプルコードなどのドキュメントの整備も進んでいます。

現状ではSolidJS単体でのSPAとしての利用や、Webサイトの一部の機能にSolidJSを利用する、といった使い方がまずは考えられそうです。また、Jamstack構成での利用という点では、AstroがSolidJSを公式にサポートしています(Astroについては近くCodeGridでも取り上げる予定です)。

規模の大きなWebアプリケーション向けプロジェクト

規模の大きなWebアプリケーションを想定したフレームワークとして、SolidStartというプロジェクトもあります。こちらは執筆時点ではまだ"work in progress"となっており、アルファバージョンのリリースが重ねられている段階のようです。このフレームワークでは、SSG、SSRやハイドレーションにも対応します。

SolidJSのコード例

ここまで開発の背景や特徴などを挙げてきましたが、百聞は一見に如かずということで、さっそく具体的なコード例を見てみましょう。

SolidJSは他の多くのUIライブラリと同様にコンポーネント指向ですので、UIを任意の単位でコンポーネントに分割できます。以下はボタンを押すとカウントが1ずつ増えるコンポーネントです。

カウンターのコンポーネント

import { render } from 'solid-js/web'
import { createSignal } from 'solid-js'

function Counter() {
  const [count, setCount] = createSignal(0)
  const countUp = () => {
    setCount(count() + 1)
  }

  return (
    <>
      <button onClick={countUp}>+1</button>
      <div>Count: {count()}</div>
    </>
  )
}

render(() => <Counter />, document.getElementById('app'))

ここでのコンポーネントはCounterという名前の関数で、まずデータを操作するロジック部分があり、それから描画部分のJSXがあります。SolidJSのコンポーネントは常にJSXを返す関数です(クラス記法はありません)。ぱっと見ると、なんとなくReactに似ているという印象を持つかもしれませんが、よく見ると何やらReactとは違う書き方をしている箇所もあります。

コンポーネントはコンストラクター

SolidJSの公式サイトには、「コンポーネントのコードはコンストラクター内で実行される」と書かれています。コンストラクターということは、コードが読み込まれ、初期化されたタイミングの一度しか実行されません。

以下はReactのコンポーネントとの簡単な比較です。SolidJSでは、カウント数が変わって再レンダリングが起こるタイミンングではコンポーネントのコードは実行されないので、以下の例ではコンソールには一度しかcalledが出力されません。

SolidJS

function Counter() {
  const [count, setCount] = createSignal(0)
  const countUp = () => {
    setCount(count() + 1)
  }
  console.log('called')
  // ↑ 1度しか実行されない

  return (
    <>
      <button onClick={countUp}>+1</button>
      <div>Count: {count()}</div>
    </>
  )
}

Reactで書いた場合は、次のようになります。

React

function Counter() {
  const [count, setCount] = useState(0)
  const countUp = () => {
    setCount(count + 1)
  }
  console.log('called')
  // ↑ コンポーネントの読み込み時に1回、以後はボタンを押すたびに実行される

  return (
    <>
      <button onClick={countUp}>+1</button>
      <div>Count: {count}</div>
    </>
  )
}

JSXを採用しているとはいえ、SolidJSのコンポーネントやDOM操作は既存のライブラリとは異なる実装なので、現在のカウント数を表示するコードも、{count}ではなく{count()}になりますし、ほかにもこのようなさまざまな差異があります。

Playgroundで実際に動作させる

Playgroundなどで実際に動作させてみると、双方の違いをより実感できるかもしれません。

ループと再レンダリング

ではもう一つ、配列をループしながらリスト表示するコード例を見てみましょう。

リスト表示するコンポーネント

import { render } from 'solid-js/web'
import { createSignal, For } from 'solid-js'

function App() {
  const [list, setlist] = createSignal([
    { name: 'Item name' },
    //...
  ]);

  return (
    <ul>
      <For each={list()}>{(item, i) =>
        <li>{`No. ${i() + 1}`} {item.name}</li>
        // ↑ インデックスの`i`もgetter関数。
      }</For>
    </ul>
  )
}

render(() => <App />, document.getElementById('app'))

リストの内容をループしてレンダリングしている部分では、<For>というSolidJS独自のコンポーネントが使われています。この独自コンポーネントは、データの一部だけが変更された際に、必要最小限のDOM要素だけを変更するよう最適化されています。

では、「ループ処理には常に<For>を使えば良いのか?」というとそうではなく、ほぼ同じように使える<Index>というコンポーネントも存在します。どちらを使っても上記のような配列のレンダリングはできますが、扱うデータの長さや更新頻度などによって、どちらのほうがパフォーマンスが良いかが変わってきます。

map()と<For>

JSXなのだからReactのようにmap()を使ってはいけないのか、と思われるかもしれませんが、実はmap()を使っても問題なく動作します。ですが、データが変更される可能性がある(そしてそれによって画面表示も変わる)場合、map()より<For>のほうがパフォーマンスが良くなります。

詳しいことは次回以降に譲りますが、SolidJSにはこの<For><Index>のような独自のコンポーネントがいくつも備わっています。こうした独自コンポーネントを活用することでパフォーマンスが向上しますし、ユーティリティ機能などもいろいろ備わっていますので、本連載でお伝えしていきます。

ここまでのまとめ

SolidJSがどのようなライブラリなのか、まずはざっくりと見てきました。開発の経緯やコード例からもわかるとおり、SolidJSは先発ライブラリのアイデアを取り入れつつも、根幹には独自の発想があります。

次回からは、実際にWebアプリケーションを作成しながら、SolidJSの考え方や機能を具体的に取り上げていきます。