Code

ラベル DOM 操作 の投稿を表示しています。 すべての投稿を表示
ラベル DOM 操作 の投稿を表示しています。 すべての投稿を表示

2016年6月18日土曜日

DOM は遅い?なぜ Virtual DOM を使うか

 先日 DOM は遅いですか?なぜ Virtual DOM を使うかと聞かれました。DOM は遅いというか、DOM 操作が遅いです。ブラウザーには reflow, repaint などの処理があります。最初 HTML と CSS を分析して、ノードツリーをレンダリングした後に、JavaScript でダイナミックにノードの高さなどを変更したり、さらにノードを追加したりすると、ツリー中の他のノードを影響されるし、ツリーを再構築するかもしれません。それは当然遅いです。
 実際 HTML のノードを遍歴する時や、ノードの属性を取得するときは遅いというか、むしろ速いです。これは jQuery 自分の event システムを持つ理由もなります。一つの要素をクリックすると、その event は buble して、親要素を遡って、ルートまで達して、event handler を呼び出します。もしこの中に DOM 操作がなければ、10 ms ぐらいで、できるかと思います。
 ただし、もしノードの offsetHeight などの computed style を取得して、さらにその値をベースに、ノードの style.height を変更するときに、ブラウザーは大変な仕事になります。
 通常ではブラウザーは変更をキャッシュして、ある時間、ある程度貯まったら、画面を一気に更新します。なぜなら、細かい更新をすると、main thread が止まったりするから、ユーザーには不親切です。が、offsetHeight などの属性を取得する時、ブラウザーはできるだけ正しい値を返すために、すべての更新を実行しなければなりません。次、style.height を変更すると、さらに、ツリーの中の他のノードの位置を計算します。この Read + Write が多くなると、main thread が応答しない場合もあります。
 じゃ、どのように DOM 操作を早くできるかというと、いくつかの方法があります。
 1 要素のスタイルを変更する場合は、width, height などを個別に変更するではなくて、新しい css class を作って、要素に入れると、こちらの更新が一つの reflow, repaint で済みます。
 2 また、offsetHeight などの値取得する時や、getBoundingClientRect() 関数を呼び出す時、できるだけ、Read を完了したら、Write を行います。つまり、Read と Write を分けます。
 3 DOM 操作を一つ一つするではなくて、まずキャッシュして、一気に行ったほうがいいです。例えば、documentFragment を使って、すべての要素を入れてから、その fragment を DOM に入れるとか。まず、要素の display を none にして(隠して)、その後に、height などを変更してから、display: block にして、再表示するとか。

 他には、
 4 アニメーションをするときに、opacityや、transform, scale など一つの layer になる css 属性を使いましょう。また、アニメーション対象の要素の position を absolute にするとか。

 Virtual DOM を使う理由は上記の 3 と関係しています。JavaScript でサイトを操作するとき、Virtual DOM を使うと、変更がメモリーにキャッシュされます。すべての操作が終わったら、Virtual DOM から DOM に更新しに行きます。この場合、ブラウザーはすべての変更を一つの reflow, repaint でできるため、速いです。
 今 React.js 以外、他に incremental dommithril などいろいろ JavaScript があります。まぁ、開発はコードを書くだけではなくて、テストなども考えなければなりませんので、今の時点で React.js は一番かなと思います。
 
 それでは。

2016年5月22日日曜日

React.js での DOM 操作

 React.js を使うと、よくある DOM node に CSS クラスだけを設定したり、その DOM node の高さを変えたいときがあります。
 DOM 操作をする場合、componentDidMount、と componentDidUpdate 二つ箇所を考えなければなりません。最初 Component が render する場合、React.js は Virtual DOM を作成して、実際の DOM に追加されたら、componentDidMount 関数が呼び出されます。その中に、findDOMNode(this) を使ったり、this.refs.REF を使って、DOM node へのアクセスは可能です。また、component に新しい props が設定されたり、setState を読んだりすると、componentDidUpdate が最後に呼び出されます。Mount される時と同じ、DOM nodeを取得して、操作できます。
 親と子の関係について、まず子の didMount と didUpdate 関数が呼び出されて、最後に親の関数が呼び出されます。

 もし親に CSS クラスを追加する場合、setState を使うと、全 App の子 Component が re-render するので、スピードだけを比較すれば、直接 DOM を操作するほうが速いかもしれません。ただ、DOM 操作を直接やると、Virtual DOM と実際の DOM が合わなくなるので、一回やると、もう関係ある DOM の更新を全て自分でしなければなりません。App 全体の構造から見ると、それは良くないです。オススメできないです。(自分の経験から見ると、逆に面倒です。)
 この場合、shouldComponentUpdate 関数を使ったほうがいいです。まず、各 Component を作るとき、どのような props または state 値変更があったら、更新すべきかを考えたほうがいいです。その値の比較を shouldComponentUpdate の中に書いて、例えば:
    return this.props.PROP !== nextProps.PROP || this.state.STATE !== nextState.STATE;

 そうすると、親が setState を自分の state を更新して、子 Component の PROP が関係ない場合、その子の shouldComponentUpdate が false を返して、Virtual DOM の比較が行われなく済みます。
 React.js はこのような DOM 操作をまとめてします。また Browser もそのような処理が入っていて、できるだけ画面更新を一回でやりたいです。個別の DOM 操作と比べると、そんなに遅くないです。
 React.js も pure render mixin が提供しています。もし自分で shouldComponentUpdate を書きたくない場合、その mixin を使えば、同じ効果が得られます。(props で関数を子 Component 渡す時、関数の Bind を使わないように。なぜなら、bind は毎回新しい関数が返されますので、reference が更新されて、pure render mixin はいつも true となります。)

 AngularJS が最初に出た時、使い方として、directive に DOM 操作すべきとか Think in AngularJS way がありました。 React.js も同じように、できるだけ、React.js にいろんな操作をやらせるべきです。
 Think in React.js way です。

 それでは。