Sencha Touch / Ext JS アプリのロード時間の高速化
こんにちは、ゼノフィnakamuraです。
何年もの間、Web 開発者は、Yahoo の Exceptional Performance チームが定義する基準に従って Web アプリケーションをデプロイしてきました。通常のアプリケーションは、JavaScript のソースファイル、CSSスタイルシート、画像などが大量になってしまうので、ベストプラクティスは、このアセットを全てを一つのファイルにまとめて圧縮する事を指示していました。論理的には、一つの JavaScript ファイルと一つの CSS ファイルを提供すると、HTTP リクエストの全体の数を削減しますので (また、アセットの全体的なサイズも減少します) 、そのため送信は最適化されページの初期化時間が改善されます。
この基本的な作業ができる公開されているツールは様々ありますが、Ext JS と Sencha Touch で作成されたアプリケーションに対しては、Sencha Cmd ほど便利さ、依存性の管理、ビルド自動化の機能を提供するものはありません。ただ sencha app build を実行するだけで、実際に必要なフレームワークコードの部分だけをデプロイします。アプリケーションコードをビルドすると、最適化されたプロダクションビルドが出力されます。
通常は、小・中規模のアプリケーションがこのビルドとデプロイの戦略から一番メリットを感じますが、エンタプライズの開発者は追加の部品の部分を検討する必要もあります。例えば、
- エンタプライズのデスクトップアプリケーションには、通常何百もの入力フォームなどの複雑なプレゼンテーションのレイアウトがあります。とても大規模なエンタプライズアプリケーションがの JavaScript, CSS, 画像が 8MB を超えるとしたら、全てを一つのファイルに圧縮しても、違いはあるでしょうか?
- ロールベースのアプリケーションでは、エンジニアリングの興味深い問題も発生させます。「低いレベルのユーザー」は機能の10%しか利用できないとしましょう。その人に、残りの90%のコードをダウンロードさせる意味はあるでしょうか? 企業もアクセスする許可がない人に圧縮したコードを提供するリスクをおかしても大丈夫でしょうか?
明らかに、最小化のアプローチに関するベストプラクティスは、アプリケーションがある規模まで大きく複雑になると頭打ちになります。ページの初期化は、この規模でこの大量な JavaScript の読み込みや解析 (実行しなくても) を行うとブラウザが動かなくなる事もあり、パフォーマンスが下がります。
この問題に対して、明らかな解決策はありませんでした。そのため、Sencha Professional Services が最近、我々のお客様の助けになったアプローチを開発しました。これは大規模のエンタプライズアプリケーションの開発で発生する問題を全て解決するわけではありませんが、このソリューションは、JavaScript のリソースを必要な時に動的に読み込むため、Sencha Cmd を活用するので、皆様と共有することにしました。
この記事で使用されているプロジェクトファイルは GitHub でご覧になれます。
待って — 動的な JS ローディング遅くないですか?
その場合もあります — が、いくつの要因が関係します。 Yahoo が定義したベストプラクティスを見ると、そのポイントは、多くのブラウザではドメイン毎のデフォルトの同時HTTP接続の数が制限 (通常は6) されているため、HTTP リクエスト削減すると、ページのロード時間も短くできるはずだということです。 そのため、この概念によると一つのファイルは10個のファイルより速くダウンロードできますが、一つの大きいファイルが、6個のより小さいファイルのより速くダウンロードできるとは限りません。
まず、Sencha Cmd で作成された Ext JS と Sencha Touch のアプリケーションは二つの異なる環境、「開発」と「プロダクション」がある事を意識しましょう。 「開発」モードの Sencha アプリケーションは、依存性のチェーンに従って (requires コンフィグによって) 、各クラスを読み込みます。しかし、「プロダクション」のアプリケーションは、必要な依存性しか含まれていない、単一の “my-app-all.js” を読み込みます。
我々が検討した JavaScript コードをプロダクションに動的に読み込むというアプローチは、圧縮した “ビルド” (“プロダクション”モードからの) を提供するという概念と、単独のクラスを読み込む (“開発”モードから) 柔軟性とを融合させたものです。 最終的に、アプリケーションの「速さ」とは、ユーザーが UI を待たないという認識によるため、アプリケーションのロード時間を改善するようにしました。
提案したソリューション: 初期ロード時の JS ファイルを削減する
上の図をよく見て下さい。大規模なエンタプライズアプリケーションでは、初期画面に必要なのは全体のコードベースの一部分だけです。 初期ページのロードパフォーマンスを上げるために、初期画面に必要なコードだけを提供するのが理想的です。 アプリケーションが速く読み込まれると、ユーザーは喜びます。そうすれば、次の機能性のアクセスについて、いつ、どんな方法で他のコードベースの部分をリクエストするか選択できます。
この問題に対して、我々のアプローチは、カスタマーアプリケーションの初期ロード時間を改善する事にフォーカスしました。注:このアプローチは、この特殊な状況に関して作成されたもので、全てのシナリオに合わないかもしれません。選択的 CSS デプロイ、コントローラーの遅延初期化、非同期のローディングメカニズムなどに関するアイディアには取り組みませんでした。
しかし、アプローチの細かい説明に入る前に、まずこの技術が適切な状況を説明しましょう。
- 「フロントページ」ビューの初期ロード時間の短縮
- 性能の低いモバイルデバイス (例えば、Android 2.x) への影響が最も大きくなります
- 初期とその後の「フロントページ」ビューの読み込み時を改善します:両方ともより少ない JS コードを解析し、その後のロードは通常 (もし適切に設定されていれば) キャッシュから行います
- 選択的に JS コードを提供する
- あるコードを実行する許可が無いユーザーには、コードを提供しないように、セキュリティに重点を置いたポリシーをサポートします。
- あるユーザーのセッションでは、コードベースの一部分だけ利用される、非常に大規模な (3MB〜) アプリケーションに最適です。
サンプルアプリの設定
この記事で利用されたプロジェクトファイルは GitHubでご覧になれます。
Sencha Cmd を利用して、基本的な “sencha generate app” のコマンドを実行すると、二つのタブパネルがある、基本的なプロジェクトの Sencha Touch 2.3.1 のアプリを生成します。 ここでは「ChunkCompile」という名前にしましょう。このサンプルで、二つ目のタブを変更します。このタブにはリストと、ユーザーが二つ目のタブに移行した後に定義をダウンロードできるカスタムコンポーネントが含まれています。
app/view/Main.js で、次が items 配列に含まれています。
items: [{ xtype : 'container', title : 'Welcome', iconCls : 'home', scrollable : true, html : 'Switch to next card for dynamic JS load...', items: { docked : 'top', xtype : 'titlebar', title : 'Welcome to Sencha Touch 2' } },{ xtype : 'container', title : 'NEXT CARD', itemId : 'nextCard', iconCls : 'action', layout : 'fit', items: [{ docked : 'top', xtype : 'titlebar', title : '"Next Card"' }] }] |
二つ目の item は TitleBar が含まれているだけの Container です。これは、プレースホルダーで次のシーケンスで実装します。
- 二つ目のタブの Tab Change イベントをインターセプトする
- ページ2の内容に必要な JavaScript をダウンロードする
- 二つ目のタブに xtype “page2” を追加する (後でこのビューを定義します)。
JS とビューを動的に読み込む
さて、既に “page2.js” のファイルが圧縮されていて、”page2″ ウィジェットに関連しているコードしか含まれてなくて、使用可能な状態になっている事を想像して下さい。これを動的にロードし、関連したビューを表示するにはどうすればいいと思いますか?
実は、Ext.Loader.loadScriptFile() というメソッドがあります。このユーティリティを app/controller/Main.js で利用します。
Ext.define("ChunkCompile.controller.Main", { //... onMainActiveCardChange: function(mainPanel, newItem, oldItem) { // Skip if 2nd page loaded already if (newItem.down('page2')) { return; } // ビルドされたアプリであれば page2 を動的にロードする if (ChunkCompile.isBuilt && !ChunkCompile.view.Page2) { // 同期で JS をロード Ext.Loader.loadScriptFile( 'page2.js', function() { console.log('SUCCESS'); }, function() { console.log('FAIL'); }, this, true ); } // "Page 2" ビューを表示 newItem.add({ xtype: 'page2' }); } }); |
ここで行っている事は、次のシーケンスにまとめられます。
- ユーザーがタブを変更すると、”main” ビューの ”activeitemchange” をインターセプトします。
- 条件式を利用し、”Page 2″ JS を動的に読み込む必要があるか判断します。
- “ChunkCompile.isBuilt” は、ビルド処理中に挿入するフラグです (後で説明します) :まだ作業できるコンパイルされたファイルがないため、開発モードでは、カスタムの “動的 JS ロード” パターンは利用しません。
- “ChunkCompile.view.Page2” は、Page2 のビュー定義の存在を確認するもので、これは JS 定義が適切に読み込まれた時点で存在します。
- このロジックはどんなものでもいいです。紹介するサンプルでは、Page2 JS がいつ読み込まれたかを確認するための最も基本的な方法です。
- もし全てのチェックが通ったら、Ext.Loader.loadScriptFile(‘page2.js’, …) を呼び出します。同期で実行させるために最後に”true” パラメータをつけています。
- 最後に、xtype “page2” で二つ目のタブにビューを追加しています。
ビュー
さて、パッケージを動的に読み込む方法がわかりましたので、Page2 と CustomComponent クラスがどう定義されているか確認しましょう。このサンプルに関して、わざとこのクラスをシンプルにしましたが、複数のファイルを利用し、この技術を表したいと思いました。
// @tag Page2 Ext.define('ChunkCompile.view.Page2', { extend : 'Ext.Container', xtype : 'page2', requires : [ 'Ext.layout.VBox', 'Ext.List', 'ChunkCompile.view.CustomComponent' ], config: { layout : 'vbox', items : [{ xtype: 'customcomponent' },{ xtype : 'list' }] } }); |
// @tag Page2 Ext.define('ChunkCompile.view.CustomComponent', { extend : 'Ext.Component', xtype : 'customcomponent', config : { html: 'Hello! This view was loaded dynamically!' } }); |
ご覧の通り、このクラスについては特別なものはなにもありませんが、 (Main ビューには存在しない) 両方のファイルの頭の “// @tag Page2” のコメントに注目して下さい。ビルド実行時にこれを参照します。
Building page2.js と all-the-rest.js
Sencha Cmdのあまり知られてないところですが、とても強力で柔軟性がある連結機能 “sencha compile” コマンドがあります。この機能は、Multi-Page and Mixed Apps というガイドで、詳しく説明されています。簡単に言うと、強力で柔軟なクエリのメカニズムを利用すると、いくつかのコードの組み合わせを別々な JavaScript ファイルに保存する事ができます。次の例で言うと、ソースコードのコメント検索とクラス名前空間のフィルタリングを活用します。
アプリのディレクトで、次のコマンドを実行します。
sencha compile \ union --recursive --file=app.js and \ save allcode and \ \ exclude --all and \ include --tag Page2 and \ include --namespace Ext.dataview and \ save page2 and \ \ concat -y build/chunked/page2.js and \ \ restore allcode and \ exclude --set page2 and \ \ concat -y build/chunked/rest-of-app-and-touch.js |
このコマンドを実行すると、二つのファイルが生成されます:
- build/chunked/page2.js
- build/chunked/rest-of-app-and-touch.js
これは、動的ローディングのアプローチに必要なものです。次は、既存のビルド処理で sencha compile を変更する方法を検討しましょう。
もし sencha compile が利用するオプションについてより詳しく知りたければ、Ext JS ドキュメントの Sencha Compiler Reference と Multi-Page and Mixed Apps を参考にして下さい。
ビルド処理にインテグレーションする
通常の “sencha app build” ではなく “sencha compile” を利用しましたね。そのため、いくつかの通常のビルド手順 (例えば、CSS の生成) は実行されませんでした。最終的に、通常のビルド処理のこの部分も自動化したいので、”sencha app build” を実行する度に、”sencha compile” コマンドライン呼び出しのカスタムオプションを挿入する必要があります。どのようにこれを実現できるでしょうか?
.sencha/app/js-impl.xml 内の “-compile-js” Ant ターゲットをご覧ください。ハイレベルな構造です。
<target name="-compile-js" depends="-detect-app-build-properties"> <if> <x-is-true value="${enable.split.mode}"/> <then> <x-compile refid="${compiler.ref.id}"> ... </x-compile> </then> <else> HERE </else> </if> </target> |
HERE と記述した場所で “sencha compile” コマンドが呼び出されるようにしますので、Ant 形式に合わせて、元々のパラメータを変更しました。
<x-compile refid="${compiler.ref.id}"> <![CDATA[ restore page and ${build.optimize} and save allcode and exclude -all and include -tag=Page2 and include --namespace=Ext.dataview and concat -y -out=${build.dir}/page2.js ${build.concat.options} and restore allcode and exclude -tag=Page2 and exclude --namespace=Ext.dataview and concat ${build.compression} -out=${build.classes.file} ${build.concat.options} ]]> </x-compile> <echo file="${build.classes.file}" append="true"> ChunkCompile.isBuilt = true; </echo> |
“ChunkCompile.isBuilt = true;” を main app.js ファイルの一番下で出力しています。動的な JS 読み込みが “production” モードだけで動作するようにするために、この値はコントローラーで、テスト条件として使われていた事を覚えていますか?
結果のファイルサイズ
js-impl.xml を編集する前に、サンプルアプリケーションで sencha app build package を実行すると、一つの 515 KB の JS ファイルが出力されました。これは、フレームワークの必要な部分と、アプリケーションコードしか含まれていません。
動的ローディングの技術を実装した後、app.js の初期ロードは 477 KB まで下がりました — 約10%を節約しました。次の page2.js の読み込み (ユーザーが二つ目のタブに移行したら) は、たったの48 KB で、一瞬で行われます。
サンプルアプリケーションで利用したコードは適当に作ったものだけですが、ロード時間の節約はすぐに効果があります。最近、性能の低いモバイルデバイス上で動作する3MB以上のアプリケーションコードがあった大規模の金融関係のアプリケーションの作成に協力しました。今日説明した技術を利用し、元々 5秒だったロード時間を1秒以下まで下げる事ができました。最も良い事は制限のないことです。アプリケーションがどんなに大きくなっても、この技術を利用できます。
まとめ
ますます大規模な HTML アプリケーションが、シングル ページ アプリケーションの概念に従うようになると、これまで頼ってきた Web アプリケーションの作成方法はもう適切ではない事が現実です。そのようなアプリケーションを作成するために必要な JavaScript は、この数年間でとても拡大しましたが、残念なことに、それを管理するツールは遅れていました。Sencha Cmd があると、この問題を解決できますし、あなたのコードの圧縮とセグメント化する方法を変更できるように、より多くのカスタマイズできる機能があります。
この解決策は、大規模なエンタプライズアプリケーションを作成する時に出会う問題を全て解決できませんが、いくつかは解決できると思います。もしあなたのアプリケーションに同様の問題があった場合、下記のコメントセクションで教えてください。あなたの解決方法もお聞きしたいです。(訳注: Sencha Blog の方に投稿して下さいね)
このアプローチや皆様の独特な状況に関するアプローチを検討したい人は、Sencha Professional Servicesに連絡して、コンサルテーションを受けて下さい。