爆速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-dommercuryを作成したMatt EschとJake Verbatenにはもう一度感謝の意を送ります。Elmがあるのは彼らのおかげでです。