先日 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 dom、mithril などいろいろ JavaScript があります。まぁ、開発はコードを書くだけではなくて、テストなども考えなければなりませんので、今の時点で React.js は一番かなと思います。
それでは。
Code
2016年6月18日土曜日
2016年6月17日金曜日
2016年6月5日日曜日
Angular から React.js への移行
昨日今やってるプロジェクトと似ている新しいウェブアップがリリースされました。AngularJS 1.0 を使っています。マネージャーたちは使ってみたら、なぜうちのプロジェクトと比べると、遅いと感じられるかと聞かれました。そもそも AngularJS 1.0 は重いからです。ぐるぐる回るロードアイコンが止まったり、なかなか応答が返ってこなかったりしたので、そう聞かれても仕方がありませんでした。
じゃ、どうやって、その AngularJS プロジェクトを React.js に移行するかと考えました。
まず、AngularJS 1.0 で ng-repeat を使ってるところを directive にして、React.js を使ってレンダリングしたほうがいいと思います。原因は ng-repeat は超を付くほど重いからです。key を使っても、ajax から返ってきたデータは毎回違うから、HTML の reuse がほぼ不可能です。逆に、React.js の場合、DOM ノードは Virtual DOM をベースに更新されるので、データが変わるなら、その分のみ更新されます。DOM ノードはそのままを使うケースが結構あります。
具体的には、ng-repeat の Collection を prop として、Component に入れます。その中で、Rendering する。directive の作り方は:
.directive('react', () => {
return {
scope: { data: '=' },
link: function (scope, element, attrs) {
scope.$watch(data, onDataChange);
function onDataChange () {
React.render(
React.createElement(REACT_COMPONENT, { data: data }),
element[0]);
};
}
}
});
これで、data が変更するたびに、React.render が呼び出されます。Virtual DOM を使っていますので、複数 rendering しても、変更する部分だけ更新されます。
次は ng-include や ng-view を使ってる部分を切り出して、React.js に個別にレンダリングします。ReactDOM.render 関数や、React.js 自身は同じページに複数の Component をレンダリング機能がサポートしています。ng-include などは独立な Component らしいので、controller の関数を React.js Component に入れて、データバインディングを完成すれば、すぐ使えます。
基本的に、service や、factory 部分を Flux や Redux の Store に変わるので、そんなに変更はないはずです。ただ、$http みたいな機能は React.js 提供してないから、qwest など別のライブラリを使ったほうがいいでしょう。これを考えると $http の promise が resovle されたら、$rootScope.$applyAsync 関数が呼ばれますので、ページ全体の Watcher が2回呼ばれます。。。重いですね。
最初の一歩は一番難しいですが、変更するたびに、どんどん新しい React.js Component もできるので、やりやすくなります。最近は React.js Develop tool を使って、他のサイトはどうやっているかが見れます。
Chrome React.js Developer tool
例えば Airbnb などは一つ一つ Component を作って、画面に入れ替わっていることが見れます。
じゃ、どうやって、その AngularJS プロジェクトを React.js に移行するかと考えました。
まず、AngularJS 1.0 で ng-repeat を使ってるところを directive にして、React.js を使ってレンダリングしたほうがいいと思います。原因は ng-repeat は超を付くほど重いからです。key を使っても、ajax から返ってきたデータは毎回違うから、HTML の reuse がほぼ不可能です。逆に、React.js の場合、DOM ノードは Virtual DOM をベースに更新されるので、データが変わるなら、その分のみ更新されます。DOM ノードはそのままを使うケースが結構あります。
具体的には、ng-repeat の Collection を prop として、Component に入れます。その中で、Rendering する。directive の作り方は:
.directive('react', () => {
return {
scope: { data: '=' },
link: function (scope, element, attrs) {
scope.$watch(data, onDataChange);
function onDataChange () {
React.render(
React.createElement(REACT_COMPONENT, { data: data }),
element[0]);
};
}
}
});
これで、data が変更するたびに、React.render が呼び出されます。Virtual DOM を使っていますので、複数 rendering しても、変更する部分だけ更新されます。
次は ng-include や ng-view を使ってる部分を切り出して、React.js に個別にレンダリングします。ReactDOM.render 関数や、React.js 自身は同じページに複数の Component をレンダリング機能がサポートしています。ng-include などは独立な Component らしいので、controller の関数を React.js Component に入れて、データバインディングを完成すれば、すぐ使えます。
基本的に、service や、factory 部分を Flux や Redux の Store に変わるので、そんなに変更はないはずです。ただ、$http みたいな機能は React.js 提供してないから、qwest など別のライブラリを使ったほうがいいでしょう。これを考えると $http の promise が resovle されたら、$rootScope.$applyAsync 関数が呼ばれますので、ページ全体の Watcher が2回呼ばれます。。。重いですね。
最初の一歩は一番難しいですが、変更するたびに、どんどん新しい React.js Component もできるので、やりやすくなります。最近は React.js Develop tool を使って、他のサイトはどうやっているかが見れます。
Chrome React.js Developer tool
例えば Airbnb などは一つ一つ Component を作って、画面に入れ替わっていることが見れます。
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 です。
それでは。
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 です。
それでは。
2016年5月1日日曜日
React.js Web App アーキテクチャー
React.js を使うと、よく変数は Component の props により親からセットするか、State に入れて、操作するかと相談されます。また、アプリ全体として、どのように作ればいいかと度々聞かれます。
今まで作った React App を例として:
DOM に Render するとき <ExampleApp {...props} />
ExampleApp の中に store からデータを取得して、state として保持。
App 中の Component はできるだけ props を使って、データまたは callback 関数をセット。
子 Component の中に、props を使って、データを表示したり、親の callback を読んだりする。
必要であれば、子 Component は state も保持する。
理由としては、React.js で Component を更新する場合、props をセットしなおす、setState({newstate}) 関数読む二つ手段があります。ExampleApp に store のデータを state に保存しているので、データが変わったら、App の setState を読んで、render 関数がもう一度呼び出して、新しい props がセットされるため、すべての子 Component が Re-render して、更新します。また子 Component 自分の state を更新する場合、子 Component の render 関数だけ呼ばれますので、App 全体のリセットが行われなくて済むので、速いです。
注意点として、まず、props が使えるなら、props を使うべきです。state の場合、re-rendering が発生する場合、保持されますので、使うときは気をつけないと。
次、props は readonly であるべきです。props は親から渡されるので、object の 場合、reference なので、もし間違って変更したら、親持ってるデータも同時に変更されますから、次 render する場合、props が変わってないようになったら、Component が更新されない可能性が高いです。
子 Component から、親に onClick や、他の Component を更新すべきと通知する場合は、親からの callback を呼ぶべきです。
onClick: function (e) { /* 子 Component 自分の操作 */
this.props.parentCallback(e); }
では、何を子 Component state に保持すべきかとういうと、例として checkbox とか form control など。その state は自分だけ使う、他の Component はそれを知らなくていいものです。もし同階層の他の Component がその値に従って、変更する場合は、親に state を保持して、props として、二つ Component に渡すべきです。
それでは。
今まで作った React App を例として:
DOM に Render するとき <ExampleApp {...props} />
ExampleApp の中に store からデータを取得して、state として保持。
App 中の Component はできるだけ props を使って、データまたは callback 関数をセット。
子 Component の中に、props を使って、データを表示したり、親の callback を読んだりする。
必要であれば、子 Component は state も保持する。
理由としては、React.js で Component を更新する場合、props をセットしなおす、setState({newstate}) 関数読む二つ手段があります。ExampleApp に store のデータを state に保存しているので、データが変わったら、App の setState を読んで、render 関数がもう一度呼び出して、新しい props がセットされるため、すべての子 Component が Re-render して、更新します。また子 Component 自分の state を更新する場合、子 Component の render 関数だけ呼ばれますので、App 全体のリセットが行われなくて済むので、速いです。
注意点として、まず、props が使えるなら、props を使うべきです。state の場合、re-rendering が発生する場合、保持されますので、使うときは気をつけないと。
次、props は readonly であるべきです。props は親から渡されるので、object の 場合、reference なので、もし間違って変更したら、親持ってるデータも同時に変更されますから、次 render する場合、props が変わってないようになったら、Component が更新されない可能性が高いです。
子 Component から、親に onClick や、他の Component を更新すべきと通知する場合は、親からの callback を呼ぶべきです。
onClick: function (e) { /* 子 Component 自分の操作 */
this.props.parentCallback(e); }
では、何を子 Component state に保持すべきかとういうと、例として checkbox とか form control など。その state は自分だけ使う、他の Component はそれを知らなくていいものです。もし同階層の他の Component がその値に従って、変更する場合は、親に state を保持して、props として、二つ Component に渡すべきです。
それでは。
2016年4月10日日曜日
React.js Dev version Warning の使い方
React.js を使うと、Webpack か Browserify と一緒に使うのがオススメです。個人的には Webpack のほうがすきです。時々 Dev バージョンに Warning が表示されたりして、Production にはそういう Warning が表示されません。それは
("production" !== process.env.NODE_ENV) ? Warning : other-process;
に秘密を隠れています。
Webpack を使うと、最後の package には、Node みたいに process.env というコードが追加されます。production バージョンをコンパイルする場合、
plugins: [
new webpack.DefinePlugin({
'process.env': {
'NODE_ENV': JSON.stringify('production')
}
})
],
が使うので、NODE_ENV は "production" になります。それで、Warning とかが実行されないようになります。
実際のコードは Uglify を通すと、false になるところが全部消されるので、production バージョンのコードには残りません。
通常の開発では、活用できるかなと。
それでは、また。
("production" !== process.env.NODE_ENV) ? Warning : other-process;
に秘密を隠れています。
Webpack を使うと、最後の package には、Node みたいに process.env というコードが追加されます。production バージョンをコンパイルする場合、
plugins: [
new webpack.DefinePlugin({
'process.env': {
'NODE_ENV': JSON.stringify('production')
}
})
],
が使うので、NODE_ENV は "production" になります。それで、Warning とかが実行されないようになります。
実際のコードは Uglify を通すと、false になるところが全部消されるので、production バージョンのコードには残りません。
通常の開発では、活用できるかなと。
それでは、また。
React.js のほうがいい、Angular 1.0 と比べたら
つい、来週新機能のリリースが決まりました。同時に、React.js のほうが、Angular JS 1.0 より良いと思われるようになりました。
以前、Framework として、AngularJS 1.0 のほうが上だと思っていましたが、React.js 15.0 のリリースより、DOM が綺麗になったことで、React.js のほうが Better になりました。
まず、data-reactid という属性がなくなりました。次、React.js によりフリーテキストに追加された<span> タグもなくなりました。これは React.js 最後の二つ問題点だと思っていましたから。これで、DOM は前より軽量に、本当の DOM のままになりました。逆に、AngularJS 1.0 の場合、ng-scope など、いろんな要素が追加されて、複雑です。
もう一つ、最大な決め手は React.js のほうが速いです。以前 AngularJS を使うとき、ぐるぐる回る gif の Spinner 止まったり、アニメーションがカクカクしたりすることが結構ありましたが、React.js の場合、一度も見たことがありません。
今作ってるサイトは競合サイトが結構あって、そちらは大体 AngularJS を使っています。同じ Spec のコンピューターを使うと、遅いなと思われました。特に Google Maps API で Marker を地図に入れる処理は UI が止まったようです。それは良くない User Experience です。
さらに、React Native も少し見てみたので、Cordova とか Sencha Touch とかと違って、これは本当の Native App になります。HTML5 Hybrid App 一つの問題は遅いです。React Native を使うと、同じ概念で、本当の App が作れるし、Native のアップだから、多少無駄なコードがあっても、全然問題ないはずです。
重要なことは一つのライブラリを使って、Web、iOS、Android 全部カバーできることです。さらにサーバーサイドレンダリングもできるので、一つのエコシステムになるような気がしています。
これから、時間をみて、今使ってる React.js のバージョンを 15.0.1にして、早くアプグレードしようと思います。
それでは。また。
以前、Framework として、AngularJS 1.0 のほうが上だと思っていましたが、React.js 15.0 のリリースより、DOM が綺麗になったことで、React.js のほうが Better になりました。
まず、data-reactid という属性がなくなりました。次、React.js によりフリーテキストに追加された<span> タグもなくなりました。これは React.js 最後の二つ問題点だと思っていましたから。これで、DOM は前より軽量に、本当の DOM のままになりました。逆に、AngularJS 1.0 の場合、ng-scope など、いろんな要素が追加されて、複雑です。
もう一つ、最大な決め手は React.js のほうが速いです。以前 AngularJS を使うとき、ぐるぐる回る gif の Spinner 止まったり、アニメーションがカクカクしたりすることが結構ありましたが、React.js の場合、一度も見たことがありません。
今作ってるサイトは競合サイトが結構あって、そちらは大体 AngularJS を使っています。同じ Spec のコンピューターを使うと、遅いなと思われました。特に Google Maps API で Marker を地図に入れる処理は UI が止まったようです。それは良くない User Experience です。
さらに、React Native も少し見てみたので、Cordova とか Sencha Touch とかと違って、これは本当の Native App になります。HTML5 Hybrid App 一つの問題は遅いです。React Native を使うと、同じ概念で、本当の App が作れるし、Native のアップだから、多少無駄なコードがあっても、全然問題ないはずです。
重要なことは一つのライブラリを使って、Web、iOS、Android 全部カバーできることです。さらにサーバーサイドレンダリングもできるので、一つのエコシステムになるような気がしています。
これから、時間をみて、今使ってる React.js のバージョンを 15.0.1にして、早くアプグレードしようと思います。
それでは。また。
登録:
投稿 (Atom)