[Elm] Html.Lazyによるパフォーマンス最適化

Elm Advent Calendar 2016 の10日目の記事です。

今回は、Html.Lazy モジュールについて。

## Html.Lazy とは

Virtual DOM のパフォーマンスがいくら良いとは言え、無駄な Virtual DOM を際限なく作り続けるとコストになります。そこで、[Html.Lazy](http://package.elm-lang.org/packages/elm-lang/html/2.0.0/Html-Lazy) モジュールはこのコストを抑え、パフォーマンスを最適化する方法を提供します。


### 仕組み

一言で言うと「モデルが変わらなければビューも変わらない」という性質を利用します。Elm の関数はすべて純粋であることが保証されているため、引数が変わらなければ返す結果はいつも同じです。そこで、Elm ランタイムは`Html`を生成する関数の引数が前回と同じであるかを見て、同じであればロジックを丸ごとスキップして前回の値を使いまわします。

### 使い方

使い方は簡単です。ビューを生成する関数の頭に`lazy`とつけるだけです。

```haskell
-- 最適化なし
view model =
  viewSomething model.something
```
```haskell
-- 最適化あり
import Html.Lazy exposing (lazy)

view model =
  lazy viewSomething model.something
```

[TodoMVC](https://github.com/evancz/elm-todomvc/blob/master/Todo.elm#L211) でも使われています。

`lazy`は特別なキーワードではなく、関数として定義されています。引数の数に応じて`lazy`、`lazy2`、`lazy3`の3つが用意されています。

```haskell
lazy : (a -> Html msg) -> a -> Html msg

lazy2 : (a -> b -> Html msg) -> a -> b -> Html msg

lazy3 : (a -> b -> c -> Html msg) -> a -> b -> c -> Html msg
```

## Html.Lazy を使う上での注意点

注意は、引数の同一性を**「参照の一致」**によって判定していることです。`lazy`関数のドキュメントの一部を引用すると次のように書いてあります（強調筆者）。

> During diffing, we can check to see if model is **referentially equal** to the previous value used, and if so, we just stop.

つまり、`view`関数内で動的に生成した値は参照が前回と一致しないため、引数に含めてはいけません。次の例は動的に新しいレコードを生成しているため、`lazy`は動作しません。

```haskell
-- 無効
view model =
  lazy viewSomething { value = model.something }
```

ただし、以下は値が同じであれば参照が一致すると考えて大丈夫です。

* 数値
* 文字列
* 型コンストラクタ（`True`, `False`, `Just`, `Nothing`, `[]` など。`Just a` は別参照になる。)

また、ビューを生成する関数も動的に生成されたもの（あるいはラムダ式）であってはいけません。

```haskell
-- 無効
view foo bar baz oneMoreThing =
  let
    f a b c =
      viewSomething a b c oneMoreThing
  in
    lazy3 f foo bar baz
```
こうすると、`oneMoreThing`が変更されたことを`lazy3`が検知できなくなってしまいますので、こうした抜け道は無効になっています。

### 参照を一致させるための工夫

上のような注意事項があるので制限がきつい気がしますが、工夫することで何とかなることも多いです。以下の例では、関数を適切にリファクタリングすることで`lazy`を有効にする例です。

#### 例１: 定数を`let .. in`中に書かずにトップレベルに宣言する

定数を毎回生成するのは勿体ないです。

```haskell
-- 無効
view model =
  let
    value = Just 1
  in
    lazy2 viewSomething model.something value 
```

定数をトップレベルに持っていくことで、参照は毎回同じになります。

```haskell
-- 有効
view model =
  lazy2 viewSomething model.something value 

value =
  Just 1
```

#### 例２: `Msg`を生成して渡さない

動的に生成した Msg を渡すと参照が新しくなってしまいます。

```haskell
-- 無効
view person =
  lazy viewSomething (Click person.id)

viewSomething msg =
  div [] [ onClick msg  ]
```

次のように関数内で組み立てるか、

```haskell
-- 有効
view person =
  lazy2 viewSomething Click person.id

viewSomething toMsg id =
  div [] [ onClick (toMsg id) ]
```

`Html.map`を使って、ビューを生成した後で Msg を変換します。

```haskell
-- 有効
view person =
  viewSomething
    |> Html.map (\_ -> Click person.id)

viewSomething =
  div [] [ onClick () ]
```

後者のコードからは`そもそもlazy`が消えていますが、もちろん定数であるビューもトップレベルに置けば`lazy`と同じように前回の値が使いまわされるため、意味的には同じです。

### lazy が有効かどうかを確認する

lazy を正しく使えているかを確認する効率的なやり方があればいいのですが、今のところそのような方法はないと思います。愚直ですが手っ取り早いのは、「ビュー関数内に`Debug.log`を仕掛けて、意図せず関数が呼ばれていないかをチェックする」です。別の方法としては、「ブラウザのプロファイル機能を使って、（以下同様）」です。後者の方がコードを弄らなくていいのと、潰すべき残りの関数が明らかになるというメリットがありそうです。

## Reactとの比較

Reactにも同様の仕組みがあり、同様の注意点があるようです。

### shouldComponentUpdate() による手動のチューニング

Reactのコンポーネントには[`shouldComponentUpdate()`](https://facebook.github.io/react/docs/react-component.html#shouldcomponentupdate)という関数が用意されています。デフォルトは`true`を返しますが、この関数をオーバーライドして適切に`false`を返すことで、レンダリングをスキップします。

まさに`Html.Lazy`と同様に「前回の状態と同じであればレンダリングしない」という戦略のためにあるのですが、その判定ロジックはユーザーにゆだねられています。

### PureComponent を使う

その後、Reactに追加された[`PureComponent`](https://facebook.github.io/react/docs/react-api.html#react.purecomponent)という仕組みを使うと、`shouldComponentUpdate()`を自動化することが出来ます。`PureComponent`は、外部から与えられる引数`props`と自身の状態`state`の変化を見ることで、レンダリングが必要かどうかを自動的に判定します。

判定ロジックはドキュメントを見ると次のように書いてあります（強調筆者）。

> React.PureComponent's shouldComponentUpdate() only **shallowly compares** the objects. 

どの程度まで「浅く」なのか微妙に分からなかったので調べると、`PureComponent`の前身である`shallowCompare`と同じ挙動をすると[書いてありました](https://facebook.github.io/react/docs/shallow-compare.html)。以下に引用します（強調筆者）。

> shallowCompare performs a shallow equality check on the current props and nextProps objects as well as the current state and nextState objects.
It does this by iterating on the keys of the objects being compared and returning true when **the values of a key in each object are not strictly equal**.

`props`と`state`の各キーに対して"strictly equal"とあることから、コンポーネントに与えた引数に対する判定ロジックはElmの`Html.Lazy`と同じと考えて良さそうです。

### 使用上の注意

[`PureComponent`のドキュメント](https://facebook.github.io/react/docs/react-api.html#react.purecomponent)によると、使用者は以下のことに注意するべしと書いてあります。

* 更新前後のオブジェクトを比較できるように、Immutableなデータ構造を検討する
* `PureComponent`の子供はすべて"pure"である必要がある

これらは両方ともElmでは解決済みの問題と言えるでしょう。ElmではすべてのデータがImmutableであり、全ての関数がビューを含めて"pure"です。ビューに状態を持てないので、例えば"時計コンポーネント"を置くためには現在時刻をモデルから引くしか方法がなく、ある意味で面倒です。しかし、この性質のおかげで「誤って`PureComponent`の下に置かれた"時計コンポーネント"が動かない」などというトラブルとは無縁でいられます。これは一種のトレードオフと言えるでしょう。

## 実際にHtml.Lazyを使ってみた

現在会社で取り組んでいる[アプリ(OSS)](https://github.com/WorksApplications/office-maker)でパフォーマンスが気になってきたので、[実際に使ってみました](https://github.com/WorksApplications/office-maker/commit/7808d5c7df935184f8523826afe80d6cbfa2df99)。

参照なんてそうそう一致しないだろうと高をくくっていたのですが、やってみると意外と使える部分が多かったです。というのは、あるビューで`lazy`が使えなくても（一番大きな`Model`を渡している関数が結構ある）末端のビューに近づくほど引数の粒度が小さくなっていくため、途中のどこかで参照が一致する事が多くありました。

効果は時間が足りずまだきちんと測れてませんが、プロファイルで見ると不要な処理がかなり減っていました。以下はあるオブジェクトを画面上でドラッグした時の様子です。

![before-after.png](https://qiita-image-store.s3.amazonaws.com/0/109182/884d2109-f86a-e829-6614-baa2a3f1514b.png)

体感としても心なしか軽くなった気がします。

## まとめ

Html.Lazy は注意点さえ押さえれば、簡単に信頼性のあるパフォーマンス最適化が可能です。ぜひ使いこなしましょう。


## 参考文献

* [Blazing Fast HTML (Round Two)](http://elm-lang.org/blog/blazing-fast-html-round-two)
* [React.js pure render performance anti-pattern](https://medium.com/@esamatti/react-js-pure-render-performance-anti-pattern-fb88c101332f)