はじめに
Reactの書き味は気に入っているけれど、大量のデータをリアルタイムに描画すると急にカクつく——そんな経験はないでしょうか。仮想DOMの差分計算そのものがボトルネックになると、memoやuseCallbackを積み重ねても限界が見えてきます。
Infernoは、この「描画そのものを速くする」という一点に振り切ったJavaScriptライブラリです。コンポーネントの書き方やライフサイクルはReactに驚くほど近いまま、内部の差分アルゴリズムを作り直すことでランタイム性能を引き上げています。JS Frameworks Benchmarkのようなベンチマークでも常に上位に顔を出す、玄人好みのライブラリです。
とはいえ、読むより触った方が早いと思います。ボタンを押すたびに再描画が走る様子を見れば、Infernoの手触りはすぐにつかめます。先に挙動を見たい方はこちらからどうぞ。
Infernoとは
Infernoは、クライアント・サーバー両方で高速なUIレンダリングを目指すReactライクなJavaScriptライブラリです。コンポーネント設計・単方向データフロー・ライフサイクルイベントといった概念はReactを踏襲しつつ、createVNodeによるモノモーフィックな内部表現やビット演算によるフラグ管理など、実行時性能に直結する部分を独自実装しています。
主な特徴
- 圧倒的な描画速度 - dbmonsterやUI Benchなどの標準的なベンチマークで、React・Vueを上回る結果を安定して出す
- Reactに近いAPI - クラスコンポーネント、関数コンポーネント、JSXなど既存のReact知識をほぼそのまま活かせる
- ネイティブなコントロールドコンポーネント -
input・select・textareaをReactのような制約なしに制御できる - linkEventによる最適化 - レンダーのたびにアロー関数を生成せずにイベントハンドラを結びつけられる
- 軽量 - gzip後で数KB台に収まり、古いブラウザでもポリフィルなしで動作する
インストール
npmまたはyarnでコアパッケージを導入します。
npm install inferno
# JSXを使う場合はビルド用プラグインも追加
npm install --save-dev babel-plugin-inferno
yarn add inferno
yarn add -D babel-plugin-inferno
サーバーサイドレンダリングやルーティングは別パッケージとして提供されています。
npm install inferno-server # SSR
npm install inferno-router # ルーティング
Infernoのサンプルを動かす
InfernoはReactと同じくJSXでUIを組み立てます。ただしJSXをInfernoのcreateElementに変換する必要があるため、下のサンプルでは/** @jsx createElement */というプラグマコメントで変換先を指定しています。infernoからrenderとComponentを、inferno-create-elementからcreateElementをインポートし、クラスコンポーネントのstateとボタンクリックで再描画される様子を確認してみてください。
要点だけを抜き出すと、Infernoのカウンターコンポーネントはこのように書きます。
import { render, Component } from 'inferno'
import { createElement } from 'inferno-create-element'
class Counter extends Component {
state = { count: 0 }
increment = () => {
this.setState({ count: this.state.count + 1 })
}
render() {
return (
<div>
<p>カウント: {this.state.count}</p>
<button onClick={this.increment}>+1</button>
</div>
)
}
}
render(<Counter />, document.getElementById('app'))
実際にブラウザ上で編集・実行できるのが以下のサンプルです。ボタンを連打してカウントを増減させたり、incrementの中身を書き換えて増分を変えたりして、Infernoの再描画の速さを確かめてみてください。
this.setStateを呼ぶたびにInfernoが差分だけを検出してDOMを更新しています。ボタンの数を増やしたり、stateに別のプロパティを足したりしても、Infernoが必要な部分だけを再計算してくれることが体感できるはずです。
基本的な使い方
Infernoの最小構成は、render関数にJSXとマウント先のDOM要素を渡すだけです。
/** @jsx createElement */
import { render } from 'inferno'
import { createElement } from 'inferno-create-element'
const message = 'Hello, Inferno!'
function App() {
return <h1>{message}</h1>
}
render(<App />, document.getElementById('root'))
クラスコンポーネントを使う場合はReactと同様にComponentを継承します。
/** @jsx createElement */
import { render, Component } from 'inferno'
import { createElement } from 'inferno-create-element'
class Greeting extends Component {
render() {
return <p>こんにちは、{this.props.name}さん</p>
}
}
render(<Greeting name="Inferno" />, document.getElementById('root'))
アプリケーションを終了させたいときは、マウント先にnullをレンダーします。これによりアンマウントのライフサイクルが呼ばれ、イベントリスナーも解放されます。
render(null, document.getElementById('root'))
実践的なユースケース
Infernoには、性能とAPIの使い勝手を両立させるための独自機能がいくつかあります。ここでは代表的な3つのパターンを、それぞれ動かせるサンプル付きで見ていきましょう。
linkEventでイベントハンドラを最適化する
Reactでリストの各項目にクリックハンドラを付けるとき、onClick={() => handleClick(item.id)}のようにレンダーのたびに新しいアロー関数を生成しがちです。これは項目数が多いと無視できないコストになります。InfernoのlinkEvent(data, callback)を使うと、dataとcallbackをペアにした軽量なオブジェクトを渡すだけで済み、レンダーごとの関数生成を避けられます。callbackは(data, event)という2引数で呼ばれます。
import { linkEvent } from 'inferno'
function handleClick(itemId, event) {
console.log('clicked', itemId, event.type)
}
// アロー関数を作らずにitemIdを紐付ける
<button onClick={linkEvent(item.id, handleClick)}>選択</button>
以下のサンプルでは、リストの各ボタンにlinkEventでアイテムのIDを紐付けています。どのボタンを押したかがログとメッセージに反映される様子を確認してみてください。
itemsの配列を増やしても、各ボタンのハンドラはlinkEventが使い回すオブジェクトを介して呼ばれるだけなので、レンダーのたびに新しい関数を作らずに済みます。
コントロールドコンポーネントでフォームを制御する
Reactではinputにvalueだけを渡してonChangeを省略すると「読み取り専用にする意図か」という警告が出ます。Infernoはこの制約を持たず、input・select・textareaをよりシンプルにコントロールドコンポーネントとして扱えます。valueとonInputを組み合わせれば、入力のたびに状態を更新してそのまま表示に反映できます。
class NameForm extends Component {
state = { name: '' }
handleInput = (e) => {
this.setState({ name: e.target.value })
}
render() {
return (
<div>
<input value={this.state.name} onInput={this.handleInput} />
<p>こんにちは、{this.state.name || 'ゲスト'}さん</p>
</div>
)
}
}
以下のサンプルでは、入力欄に文字を打つたびにstate.nameが更新され、下の表示にリアルタイムで反映されます。入力を全部消すとゲスト表示に戻る挙動も確認してみてください。
onInputは入力のたびに即座に発火するので、onChangeのようにフォーカスが外れるまで待つ必要がありません。selectやtextareaでも同じvalue+onInputの形がそのまま使えます。
createPortalでDOM階層外へ描画する
モーダルやツールチップは、親コンポーネントのDOM階層の外、たとえばdocument.body直下に描画したいことがよくあります。CSSのoverflow: hiddenやz-indexの影響を避けるためです。InfernoのcreatePortal(children, domNode)を使うと、ReactのcreatePortalと同じ感覚で、Infernoコンポーネントの状態を保ったままレンダー先だけを切り替えられます。
import { createPortal } from 'inferno'
class Modal extends Component {
render() {
return createPortal(
<div className="modal">{this.props.children}</div>,
document.getElementById('modal-root'),
)
}
}
以下のサンプルでは、ボタンを押すとcreatePortalによって#modal-root側にモーダルが描画されます。コンポーネントツリー上は親の内側にありながら、実際のDOMは別の場所に挿入されている点に注目してみてください。
Appコンポーネントの中に<Modal />を書いているにもかかわらず、実際のDOM構造では#modal-rootの下にモーダルが挿入されます。state.openの切り替えだけでモーダルの表示・非表示を制御でき、レイアウトの都合を気にせず自由な位置に描画できるのがcreatePortalの強みです。
まとめ
Infernoは、Reactで培った知識をほぼそのまま活かしながら、描画性能をとことん追求できるライブラリです。クラスコンポーネントや関数コンポーネントといった基本の書き方はReactと変わらず、linkEventによるハンドラの最適化、Reactより素直なコントロールドコンポーネント、createPortalによるDOM階層外レンダリングといった機能で、パフォーマンスとコードの書きやすさを両立させています。
大量のリストやリアルタイム更新が多い画面でReactの再描画コストが気になってきたら、置き換え先の一つとしてInfernoを試してみる価値は十分にあります。まずは既存のReactコンポーネントの一部をInfernoに移植して、ベンチマークの数値がどう変わるか確かめてみてください。