2014年8月28日
爆速HTML – Elmでの仮想DOM
本記事は、原著者の許諾のもとに翻訳・掲載しております。
新たな elm-html ライブラリでは、HTMLとCSSをElmで直接使用できます。FlexBoxも使ってみたいし、既存のスタイルシートも使い続けたいですか? Elmは使いやすくなり、処理が 速く なりました。例えば、 TodoMVC アプリを再作成する場合、Elmの コード はとても単純で、 事前のベンチマーク でも、他の人気ライブラリに比べ処理速度が極端に速いという結果が出ています。
elm-html とMercuryは、どちらも virtual-dom プロジェクトを基にしているので、パフォーマンスが優れています。この記事では、前半で“仮想DOM”とは何か、 純粋性 と 不変性 によっていかに処理速度が上がるかということについて詳しく検証します。この検証によって、なぜOm、Mercury、Elmがベンチマークでこのような素晴らしい数字を出したかが分かるでしょう。
パフォーマンスは人を引きつける良い材料ですが、本当の利点は、このアプローチによりコードが読みやすく維持しやすいものになることです。要するに、再利用可能なHTMLウィジェットを作成し、共通パターンを抽象化するのが、かなり簡単になるのです。そのため、大きなコードベースを使う人はきっと仮想DOMアプローチに興味を持つでしょうね。
このライブラリは、Elmの使用を考えていた人たちにとっても朗報です。つまり、Elmも使える上に、使い慣れたCSSとデザイナ/ディベロッパのワークフローも使い続けられるということです。かつてないほど簡単に、プロジェクトでElmの効果を得ることができます。では、実際に見てみましょう。
仮想DOM
このライブラリは“仮想DOM”を基にしています。DOMを直接触るのではなく、フレームごとにDOMの抽象化バージョンを構築します。求めるものを簡単に表現するためにノード node
関数を使います。
node : String -> [Attribute] -> [CssProperty] -> [Html] -> Html
これにより、タグ、HTML属性のリスト、CSS特性のリスト、子要素のリストを特定できます。例えば、ノード node
を使って、ユーザの写真や名前を表示するプロフィール profile
ウィジェットを構築することができます。
profile : User -> Html
profile user =
node "div" [ "className" := "profile" ] []
[ node "img" [ "src" := user.picture ] [] []
, node "span" [] [] [ text user.name ]
]
クラスを設定すると、すべてCSSでスタイル付けされます。Elmのモジュールシステムと組み合わせると、共通パターンの抽象化とコードの再利用が簡単になります。全APIと参考文献については ここ で確認できます。さらなる具体例については、 再利用可能なウィジェット のセクションで検証します。
仮想DOMの処理速度を上げる
仮想DOMというと、処理速度がとても遅そうな感じがしますよね。フレームごとに新しいシーンを作成するのでしょうか? 実は、この技術は ゲーム業界で広く使われていて 、2つの比較的簡単な技術を使えば、DOM更新が驚くほど高速で行えます。その技術とは差分検出と遅延です。
ReactはDOMをどのように変更する必要があるか把握するために“差分検出”の概念を広めました。 差分検出とは、現在の仮想DOMと新しい仮想DOMを比較して、違いを探すことです。 初めは少し手の込んだ方法に思えるかもしれませんが、処理はとても簡単です。まず、誰かが特定の <div>
の色を変更したとか、新しい <div>
を追加したというような違いのリストを作成します。すべての違いを見つけたら、その違いをインストラクションとして使用して、DOMを変更します。 requestAnimationFrame を使って、まとめてバッチ処理するのです。つまり、DOMを変更したり、処理速度が速いか確認したりするなどの手間をかかる作業を自分でやらなくてもいいのです。あなたは、読みやすく維持しやすいコードを書くことに集中できます。
このアプローチは、Elmに最適な方法でHTMLとCSSを完全にサポートする明確な方法を切り開きました。しかも、Elmは既に純粋性と不変性という優れた機能を備えており、それらは差分検出を飛躍的に速くする最適化に必要不可欠なものです。
ReactとOmによって見出されたとおり、データが不変の場合は特に、差分検出を遅延させると、パフォーマンスを大幅に改善することができます。例えば、タスクリストを表示しているとしましょう。
todoList : [Task] -> Html
todoList tasks =
node "div" [] [] (map todoItem tasks)
しかし、恐らく何度更新しても変更しているタスクはないと思っていいでしょう。タスクが変更していない場合、ビューも変更していないはずです。このタイミングで遅延させるのが理想的です。
lazy : (a -> Html) -> a -> Html
todoWidget : State -> Html
todoWidget state =
lazy todoList state.tasks
あらゆるフレームで todoList
関数を呼び出すのではなく、 state.tasks
に変更があったかを確認するのです。変更がなければ、すべてをスキップできます。関数を呼び出す必要もありませんし、差分検出を行う必要もDOMを触る必要もありません。この最適化は安全です。なぜなら、Elmの関数は 純粋 であり、データは 不変 だからです。
- 純粋性 とは、
todoList
関数が同じ入力に対して 常に 同じ出力を返すことを意味します。つまりstate.tasks
が同じであれば、todoList
を完全にとばしていいのです。 - 不変性 によって、値が「同じ」であることを簡単に知ることができます。2つの値の参照先が同じであれば、構造も 必ず 同じになります。
なので、最終フレームとして リファレンス で古い値と新しい値を比較し、todoListとstate.tasksが同じであるかを確認すればいいのです。非常に簡単ですし、同じであれば大抵の場合、遅延(lazy)関数は大量の作業を避けられます。これにより作業が大幅にスピードアップします。
Elmをしばらく使っている人であれば、あるパターンに気がつくでしょう。関数が純粋であることと、データが不変であることは、非常に重要だということです。これについては、 Elmでホットスワップ と タイムトラベル・デバッガ を読み、理解を深めてください。
再利用可能なウィジェット
このアプローチによって、再利用可能なウィジェットを驚くほど簡単に作ることができます。例えばユーザプロフィール一覧も、以下のように見事に抽象化できます。
import Html (..)
profiles : [User] -> Html
profiles users =
node "div" [] [] (map profile users)
profile : User -> Html
profile user =
node "div" [] []
[ node "img" [ "src" := user.picture ] [] []
, text user.name
]
ユーザ一覧を抜き出しHTMLを返しているプロフィール profile
のウィジェットがありますね。このウィジェットはどこでも容易に再利用できますし、テンプレート言語とは違い、Elmでは柔軟にこうしたウィジェットを作ることができます。共通のウィジェットやパターン用のコミュニティライブラリを作ることも可能です。
複雑なスタイルを作りたい場合も、それらを抽象化し再利用することが可能です。以下の例では、どんなノード上でもミックスおよび一致が可能なフォント font
と背景 background
を定義しています。
-- small reusable CSS properties
font : [CssProperty]
font =
[ "font-family" := "futura, sans-serif"
, "color" := "rgb(42, 42, 42)"
, "font-size" := "2em"
]
background : [CssProperty]
background =
[ "background-color" := "rgb(245, 245, 245)" ]
-- combine them to make individual nodes
profiles : [User] -> Html
profiles users =
node "div" [] (font ++ background) (map profile users)
今や再利用可能なウィジェットを作り共通パターンを抽象化するのは極めて簡単なことですが、それをはるかに上回ることができるのです。
抽象化の自由
私が後にElmとなるプロジェクトに取り組み始めた当時、HTML誕生から約20年が経っていましたが、人々は垂直方向にセンタリングする方法を調べるために、ブログを3つ読み、Stack Overflowに上げられた質問を5つ参照しているような状況でした。私がElmで掲げた最初の目標は、GUIをゼロから見直すことでした。 もしやり直せるなら、ウェブプログラミングはどう変わるでしょうか?
この目標を追求するにあたり、 elm-html には、非常に重要な2つの強みがあります。1つ目はHTMLとCSSにアクセスできるので、いつでも最新の機能をフルに利用できる点、そして2つ目は新たに抽象化を行うことができる点です。
つまり HTMLとCSSは、よりよい抽象化のための基本的な要素になると言えます。 例えば、このライブラリを使ってElmのエレメント(Element)の抽象物を再作成することは可能でしょう。ですが何よりも重要なのは、モジュール形式でより見栄えのいいビューにするために、誰もが新しい方法で試すことができることです。Paul Chiusanoは CSSに関する挑発的な記事 の中で、これについてとても上手に説明しています。
今も私のElmの目標は、一風変わった、ひねくれた視点からウェブプログラミングを見直すことです。その中で、HTMLとCSSが大きなステップであるという考えも100%支持しています。 elm-html の可能性を見るのが楽しみです。
アーキテクチャに関するメモ
新しいアプローチにはつきものですが、「大規模なプロジェクトになったらどうなる?」という質問をまず最初に投げかけられます。一般的なアプローチはOmやFacebookのFluxを用いた大規模なアプリケーション設計と大体同じです。 Elmでどう機能するか については概要を紹介しましたが、近いうちに、より正式な形で文書や例を作成するつもりです。
謝辞
これらの技術を発見し普及させたReactとOmに感謝します。私の理解に手を貸してくれたSebastian Markbage、David Nolen, Matt Esch、そしてJake Verbatenには特に感謝しています。
また、このライブラリのベースとなっている virtual-dom と mercury を作成したMatt EschとJake Verbatenにはもう一度感謝の意を送ります。Elmがあるのは彼らのおかげでです。
株式会社リクルート プロダクト統括本部 プロダクト開発統括室 グループマネジャー 株式会社ニジボックス デベロップメント室 室長 Node.js 日本ユーザーグループ代表
- Twitter: @yosuke_furukawa
- Github: yosuke-furukawa