目次


Web アプリケーションのパフォーマンスを改善する

クライアント・サイドのコンテンツのボトルネックを突き止めて、実行速度を向上させる

Comments

概要

リッチ・インターネット・アプリケーション (RIA) は、Web 2.0 の分野で大きな人気を集めています。新しい魅力的なユーザー・エクスペリエンスを実現するために、多くの Web サイトでは JavaScript や Flash を使用することで、サイトの複合コンテンツをバックエンド・サーバーに置くのではなく、クライアント・サイドに持たせるようになっています。コンテンツをクライアント・サイドに持つようにすれば、ユーザー・インターフェース (UI) は使い勝手が良くおしゃれなものになり、データのサイズがかなり小さい場合にはスムーズに動作します。けれどもサーバーからクライアントに大量のコンテンツを転送してブラウザーでレンダリングしなければならない場合には、パフォーマンスは顕著に劣化します。そこで課題となるのが、ボトルネックを突き止めてそれに対するソリューションを見つけることです。

ブラウザーでのパフォーマンスの調整に関する問題は、Java アプリケーションでの場合よりも困難です。開発者にとって、すべてのブラウザーで JavaScript をデバッグするために使える手段は限られているためです。Mozilla Firefox では Firebug を使って JavaScript をデバッグすることができますが、それでもまだ調整することができないパフォーマンスの問題は数多くあります (ブラウザーがレンダリングに費やす時間など)。この問題を解決するには、応答時間をモニターするブラウザー用のプラグインを開発するだけでなく、他の対応するソリューション (例えば、部分レンダリングや遅延ローディングなど) を見極めることが不可欠です。

この記事を読んで、Web アプリケーションのパフォーマンスの問題を診断し、クライアント・サイドのコンテンツのボトルネックを突き止めてパフォーマンスを調整する方法を学んでください。

JavaScript と HTML DOM

JavaScript は、インターネットで最もよく使われているスクリプト言語です。何百万人もの Web ページの設計者が、設計の改善、入力フォームの検証、ブラウザーの検査、cookie の作成に JavaScript を使用しています。一方、HTML DOM (Document Object Model) は HTML 文書にアクセスして操作するための標準的な手法を定義します。DOM は HTML 文書を、要素、プロパティー、テキスト・コンテンツで構成されるノード・ツリーとして表現します。

JavaScript は HTML DOM を使用することで、HTML 文書に含まれるすべてのノードにアクセスし、それらのノードを操作することができます。JavaScript と HTML DOM は、主流となっているほぼすべてのブラウザーでサポートされていることから、ほとんどの Web サイトで使用されています。したがって、この 2 つが使用されている間のパフォーマンスが、RIA の全体的なパフォーマンスに大きな影響を与えるということになります。

JavaScript のパフォーマンスと関数

JavaScript で関数が必要なときには、関数を使用してください。場合によっては関数ではなく、文字列を使うこともできますが、可能な限り関数を使用することをお薦めします。関数は JavaScript で使用される前にプリコンパイルされるからです。

一例として、リスト 1 に示す eval メソッドを見てください。

リスト 1. 文字列をパラメーターとして使用する eval メソッド
function square(input) {
  var output;
  eval('output=(input * input)');
  return output;
}

上記の eval メソッドは入力を二乗して出力値を返しますが、パフォーマンスにはそれほど期待できません。この例では output=(input*input) という文字列を eval メソッドのパラメーターとして使用しています。そのため、JavaScript のプリコンパイルを利用することができません。

このジョブに、よりふさわしいのは、リスト 2 の方法です。

リスト 2. 関数をパラメーターとして使用する eval メソッド
function square(input) {
  var output;
  eval(new function() { output=(input * input)});
  return output;
}

文字列の代わりに関数をパラメーターとして使用することで、new メソッドに含まれるコードは JavaScript コンパイラーによって確実に最適化されることになります。

関数スコープ

JavaScript 関数のスコープ・チェーンの各スコープには、複数の変数が含まれます。関数スコープのメリットを生かすためには、スコープ・チェーンについて理解することが重要です。リスト 3 に関数スコープの一例を示します。

リスト 3. 関数スコープ
function test() {
  var localVar = “test”;
  test1(this. localVar);
var pageName = document.getElementById(“pageName”);
}

図 1 に、スコープ・チェーンの構造を示します。

図 1. スコープ・チェーンの構造
関数スコープのメリットを生かすためには、スコープ・チェーンの構造を理解することが重要です。
関数スコープのメリットを生かすためには、スコープ・チェーンの構造を理解することが重要です。

チェーンの先に行けば行くほど、変数の解決にかかる時間は長くなります。したがって、グローバル変数を使うよりローカル変数を使ったほうが、遥かに実行時間が短縮されます。

コードに with または try-catch 文が含まれている場合、スコープ・チェーンはさらに複雑になります。図 2 に、try-catch 文のスコープ・チェーンを示します。

図 2. try-catch 文のスコープ・チェーンの構造
チェーン構造に try-catch 文が含まれていると、チェーン構造はさらに複雑になります。
チェーン構造に try-catch 文が含まれていると、チェーン構造はさらに複雑になります。

文字列関数

文字列の連結は、JavaScript で最も使用すべきでない関数の 1 つとして挙げられます。通常、文字列の連結関数は + 符号を使って文字列を連結します。リスト 4 はその一例です。

リスト 4. 文字列の連結
var txt = “hello” + “ ” + “world”;

上記のステートメントは、連結結果が含まれる複数の中間文字列を作成します。このように、裏では文字列が何度も作成されては破棄されるため、文字列を連結するには、かなりの時間がかかることになります。初期のブラウザーでは、このような連結操作の最適化は一切行われませんでした。そこで私たちが提案するのが、StringBuffer クラスを作成して実装することです (リスト 5 を参照)。

リスト 5. StringBuffer オブジェクト
function StringBuffer() {
this.buffer = [];
}

StringBuffer.prototype.append = function append(val) {
 this.buffer.push(val);
 return this;
}

StringBuffer.prototype.toString = function toString () {
 return this.buffer.join(“”);
}

上記では、プロパティーとメソッドはすべて (文字列の値に対してではなく) 文字列オブジェクトに対して定義されています。そのため、文字列の値に関するプロパティーやメソッドが参照されると、ECMAScript エンジンは、そのメソッドを実行する前に、暗黙的にその文字列の値を使って新しい文字列オブジェクトを作成しなければなりません。新しく作成されたオブジェクトはその特定のリクエストにだけ使用され、次に文字列の値に関するメソッドを使用するときには、再度新しいオブジェクトが作成されます。

この場合、何度か呼び出されるメソッドが含まれる文字列には new 文を使用してください。

リスト 6 に、新規文字列オブジェクトの一例を記載します。

リスト 6. 新規文字列オブジェクトを作成する例
var str = new String(“hello”);

StringObject.indexOf は StringObject.match よりも高速です。単純な文字列の一致を検索するときには、正規表現による突き合わせではなく、できるだけ indexOf を使用してください。

どうしても必要な場合を除き、長い文字列 (10KB 以上) に対する突き合わせは禁物です。文字列の特定の部分で一致が見つかることが確実な場合には、サブ文字列を取り、文字列全体でなく、そのサブ文字列に対して比較をしてください。

DOM のパフォーマンス

このセクションでは、DOM のパフォーマンスを改善するために調整できる操作について概説します。

再ペイント

DOM は、これまで可視でなかったものが可視になると (あるいはその逆の場合も) 常に再ペイントされます。再ペイントは、再描画とも呼ばれます。この振る舞いによって文書のレイアウトが変更されることはありません。要素のサイズ、形状、位置は変更しないけれども、要素の外観は変更する操作はすべて、再ペイントをトリガーします。

例えば、要素に外枠を追加したり、要素の背景色を変更したりすると、再ペイントが行われます。この再ペイントは、パフォーマンスの点でコストが高くつきます。それは、エンジンがすべての要素を検索して、何が新しく可視になったか、そして何を表示する必要があるかを判断しなければならないためです。

再フロー

再フローは、再ペイントよりも目立った変更です。再フローは以下の場合に起こります。

  • DOM ツリーを操作した場合
  • レイアウトに影響するスタイルの変更を行った場合
  • 要素の className プロパティーを変更した場合
  • ブラウザーのウィンドウ・サイズを変更した場合

このような場合、エンジンは関連する要素の再フローを行い、さまざまなパーツを今度はどこに表示するかを見極めなければなりません。子要素でも、その親要素の新しいレイアウトを反映するために再フローが起こります。さらに、最初の再フローによって移動されている可能性があることから、DOM 内で各要素の後にある要素でも新しいレイアウトを計算するために再フローが起こります。祖先要素についても同じことです。子要素のサイズ変更を反映するために再フローが起こり、そして最後に、全体の再ペイントが行われることになります。

文書に要素が追加されるたびに、ブラウザーはページの再フローを行って、すべてのパーツをどのように配置してレンダリングするかを解決しなければなりません。要素を追加すればするほど、再フローの負担は大きくなります。追加する個々の要素の数を減らすことができれば、ブラウザーが行うフローの回数は減り、その分処理速度も速くなります。

CSS のパフォーマンス

CSS (Cascading Style Sheets) は先頭に配置してください。スタイルシートが最後に配置されていると、スタイルシートは最後にロードされることになます。スタイルシートがロードされるまで、ブラウザーはページ上に何もレンダリングすることができません。静的テキストでさえもレンダリングできないため、数秒の間、ページはまったくブランクの状態になります。

Internet Explorer の場合、@import は <link> を最後に配置する場合と同じように振る舞うので、Internet Explorer では @import 使用しないようにすることをお薦めします。

短縮プロパティー

複数のプロパティーを設定する場合には、それぞれのプロパティーを別個に宣言するのではなく、短縮プロパティーを使用して 1 つの宣言でまとめて設定してください。短縮プロパティーを使用すると、ファイルのサイズを縮小することができます。したがって、保守の負担を軽くすることができます。

短縮プロパティーで設定できるプロパティーには、border、border-color、border-style、border-top、border-right、border-bottom、border-left、border-width、font、list-style、margin、outline、padding があります。

CSS セレクター

CSS セレクターは、右から左に向かって突き合わせられます。リスト 7 の場合、ブラウザーはページ上のすべてのアンカー要素を繰り返し処理し、その親要素の ID が aElement であるかどうかを判別します。

リスト 7. セレクターは右から左に向かって突き合わせられます
#aElement > a{
    font-size: 18px;
}

上記のコードから > を取り除くと (リスト 8 を参照)、パフォーマンスは劣化します。この場合、ブラウザーが文書全体ですべてのアンカーをチェックしなければならなくなるからです。しかも、各アンカーの親をチェックするだけでなく、ブラウザーは文書ツリーを上に辿って ID が aElement に設定された祖先要素を探さなければなりません。評価対象のアンカーが aElement の子孫でなくても、ブラウザーは文書ルートに達するまで祖先要素のツリーを辿ることになります。

リスト 8. > がないとパフォーマンスが劣化します
#aElement a{
    font-size: 18px;
}

ベスト・プラクティス

このセクションでは、Web アプリケーションを改良してパフォーマンスを向上させるためのベスト・プラクティスについて概説します。

HTTP リクエストの数を減らす

すべての HTTP リクエストには、DNS の検索、接続の作成、レスポンスの待機をはじめとしたオーバーヘッドが伴います。したがって、リクエストの数を減らすことによって、不要なオーバーヘッドを削減することができます。リクストの数を減らすには、以下の方法があります。

  • ファイルを結合する。必ず同時に必要となる複数のスクリプトを 1 つのファイルにまとめると、ファイル全体のサイズは小さくならないものの、リクエストの数は減るはずです。同じように、CSS ファイルと画像も結合することができます。ファイルの自動結合は、以下の段階で実装することができます。
    • ビルドの段階で、<concat > タグによって、Ant を実行してファイルを結合します。
    • ランタイムの段階で、mod_concat モジュールを使用できるようにします。httpServer が Apache の場合は、JSP タグ・ライブラリーに pack:Tag を使用して、JavaScript ファイルとスタイルシート・ファイルを結合します (pack:Tag は、JavaScript や CSS などのリソースを縮小、圧縮、結合して、メモリーまたは生成されたファイル内にキャッシュする JSP タグ・ライブラリーです)。
  • CSS Sprite を使用する。背景画像をまとめて 1 つの画像に結合し、CSS の background-image および background-position プロパティーを使用して目的の画像セグメントを表示します。また、インライン画像を使用してリクエストの数を減らすこともできます。

コンポーネントを後でロードする

絶対に不可欠なコンポーネントだけをレンダリングしてください。それ以外のコンポーネントは後でレンダリングするのでも構いません。多数の要素を一度にレンダリングしないようにすることが最善の策です。

場合によっては、後でロードする方法を選択することもできます。ブラウザーの表示域には入らないコンポーネントは後からロードしても問題ありません。そのため、これらのコンポーネントが表示域に初めて入ってきたと同時に、初期レンダリングを起動することができます。

JavaScript の一部は、例えば初期レンダリングの後に JavaScript を使って要素をドラッグするなど、onload イベントが発生してからロードすることができます。

コンポーネントを事前にロードする

コンポーネントを事前にロードすることによって、ブラウザーのアイドル時間を利用して、将来的に必要になるコンポーネント (画像、スタイル、スクリプトなど) をリクエストします。コンポーネントの大多数がキャッシュにロードされていれば、ユーザーが次のページにアクセスするときに、ページのロード時間が大幅に短縮されます。

事前にロードする方法には以下の 2 つのタイプがあります。

  • 無条件: onload イベントの発生と同時に、すぐには必要のないコンポーネントを事前に取得します。
  • 条件付き: ユーザーのアクションを基に、次にユーザーが何をしようとしているかを経験に基づいて推測し、それに応じたコンポーネントを事前にロードします。

スクリプトを最後に配置する

スクリプトは並行ダウンロードをブロックすることから、問題になる可能性があります。スクリプトのダウンロード中は、ブラウザーはホスト名が違っているとしても他のダウンロードを一切開始することができません。したがって、スクリプトはスタイルシートと同じく最後に配置して、他のロードが完了した後にスクリプトのダウンロードが開始するようにしてください。

遅延スクリプトを使用することもできますが、これは Internet Explorer でしかサポートされません。DEFER 属性は、そのスクリプトに document.write() が含まれていないことを示します。ブラウザーにとってこの属性が、レダリングを継続可能であることを知る手掛かりとなります。

cookie のないドメイン・コンポーネントを使用する

ブラウザーが静的画像をリクエストするときに、そのリクエストと一緒に cookie を送信しても、サーバーはこれらの cookie を一切使用しません。cookie は不要なネットワーク・トラフィックを生じさせるだけなので、静的コンポーネントをリクエストする場合には必ず cookie を使わないリクエストを使用してください。そしてサブドメインを使用して、すべての静的コンポーネントをホストしてください。

JavaScript および CSS を外部化する

実際に外部ファイルを使用すると、通常はページの動作が速くなります。その理由は、ブラウザーが JavaScript ファイルおよび CSS ファイルをキャッシュするからです。HTML 文書にインライン化された JavaScript および CSS は、HTML 文書がリクエストされるたびにダウンロードされるため、必要な HTTP リクエストの数は減りますが、HTML 文書のサイズが大きくなってしまいます。その一方、ブラウザーがキャッシュする外部ファイルに JavaScript と CSS が含まれていれば、HTML 文書のサイズが小さくなるだけでなく、リクエストの数が増えることもありません。

RIA ウィジェットのパフォーマンス

ExtJS、YUI、Dojo など、主流となっている RIA Ajax フレームワークでは、いずれもユーザー・エクスペリエンスに磨きをかける巧みなウィジェットのライブラリーを提供しています。なかでも Dojo は他のフレームワークに比べ、以下の理由により、エンタープライズ開発の分野で勝っています。

  • オブジェクト指向プログラミング (OOP) によるコーディング
  • クロスプラットフォーム
  • Dojo のオフライン API によるローカル・データ・ストレージのサポート
  • DataGrid、2D、および 3D グラフィクス (グラフ・コンポーネントによって、ブラウザーでレポートを表示する方法を簡易化)

Dojo はこれまで多くの Web サイトで広く使用されています。そこで、ここからは Dojo を使用して、RIA ウィジェットのパフォーマンスを分析する例を説明します。Dojo ウィジェットを調整するには、Page Speed、Rock Star Optimizer、Jiffy など、多数のツールを自由に使うことができます。そのなかで私たちがぜひお勧めするのは、YSlow と Firebug です。

YSlow

YSlow は、JavaScript で作成されたコンポーネントを含め、ページ上のすべてのコンポーネントを調べて Web ページのパフォーマンスを分析します。分析の基準となるのは、ハイパフォーマンス Web ページを対象とした一連のルールです。Firefox のアドオンである YSlow は、Firebug Web 開発ツールに統合されています。YSlow では、ページのパフォーマンス改善方法の提案や、Web ページのコンポーネントの要約、ページに関する統計の表示、パフォーマンス分析用ツールの提供などを行います。

図 3 は、YSlow の「Grade (グレード)」タブに表示される情報の一例です。

図 3. YSlow の「Grade (グレード)」タブ
Firefox アドオンである YSlow に組み込まれた「Grade (グレード)」パネル
Firefox アドオンである YSlow に組み込まれた「Grade (グレード)」パネル

YSlow は、22 のテスト可能なルールに基づいて Web ページを分析します。以下に、これらのルールを大体の重要性および有効性の順に記載します。以下のルールによって、Web ページの応答時間を 25 パーセントから 50 パーセント改善できるという調査結果が出されています。

  • HTTP リクエスト数を最小限に減らすこと
  • コンテンツ配信ネットワークを使用すること
  • Expires または Cache-Control ヘッダーを追加すること
  • コンポーネントを GZIP 形式で圧縮すること
  • スタイルシートを先頭に配置すること
  • スクリプトを最後に配置すること
  • CSS 式をなるべく使用しないこと
  • JavaScript および CSS を外部化すること
  • DNS ルックアップを減らすこと
  • JavaScript および CSS を縮小化すること
  • リダイレクトをなるべく使用しないこと
  • 重複するスクリプトをなくすこと
  • ETag を構成すること
  • Ajax でキャッシュできるようにすること
  • Ajax リクエストには GET を使用すること
  • DOM 要素の数を減らすこと
  • 404 を排除すること
  • cookie のサイズを縮小すること
  • コンポーネントには cookie なしのドメインを使用すること
  • フィルターを使用しないようにすること
  • HTML で画像を拡大/縮小しないこと
  • favicon.ico を小さくしてキャッシュ可能にすること

YSlow の「Statistics (統計)」タブ (図 4 を参照) では、ユーザーが初めてアクセスする場合 (キャッシュが空の状態) のページのサイズと、ユーザーが以前にアクセスしたことのあるページのサイズを比較します。

図 4. YSlow の「Statistics (統計)」タブ
ページのサイズを比較する YSlow の「Statistics (統計)」パネル
ページのサイズを比較する YSlow の「Statistics (統計)」パネル

「Components (コンポーネント)」タブには、各コンポーネントがパフォーマンスに関する情報と併せて表示されます。このタブでは例えば、コンポーネントが GZIP で圧縮されたかどうか、ETag (存在する場合) が何であるかを調べることができます。コンポーネントのサイズと有効期限も、この「Components (コンポーネント)」タブに表示されます (図 5 を参照)。

図 5. YSlow の「Components (コンポーネント)」タブ
サイズや有効期限をはじめ、多くの関連情報を表示する YSlow の「Components (コンポーネント)」パネル
サイズや有効期限をはじめ、多くの関連情報を表示する YSlow の「Components (コンポーネント)」パネル

Firebug

Firebug は Mozilla Firefox に統合され、ブラウズ中にすぐに使用できる豊富な開発ツールを提供します。これらの開発ツールを使って、あらゆる Web ページの CSS、HTML、および JavaScript を編集、デバッグ、モニターすることができます。

Web ページが開始した HTTP トラフィックをモニターするには、Firebug の「Net (接続)」パネルを使用します。このパネルは、収集および計算されたすべての情報をユーザーに表示します。各エントリーが、ページが行ったリクエスト/レスポンスの 1 回のやり取りを表します。

図 6. Firebug の「Net (接続)」パネル
ページからのトラフィックをモニターする Firebug の「Net (接続)」パネル
ページからのトラフィックをモニターする Firebug の「Net (接続)」パネル

Firebug の「Console (コンソール)」パネルでは、2 つの方法でコードのパフォーマンスをモニターすることができます。

図 7. Firebug の「Console (コンソール)」パネル
コードのパフォーマンスをモニターするのに役立つ Firebug の「Console (コンソール)」パネル
コードのパフォーマンスをモニターするのに役立つ Firebug の「Console (コンソール)」パネル
プロファイル
ある特定の関数について調べるには、プロファイラーを使用してください。JavaScript プロファイラーは、各 JavaScript コードの実行時間を測定することができる有用な Firebug の機能です。コードのパフォーマンスの改善に取り組んでいる場合、あるいは特定の関数を実行するのに時間がかかっている原因を調べる場合には、JavaScript プロファイラーが役に立ちます。JavaScript プロファイラーは console.time() と似ていますが、コードの実態に関してより詳しい情報を提供してくれます。
console.time()
ある特定のコードについて調べるには、console.time() を使用します。このメソッドは、コマンドラインに入力したコマンドの実行結果をコンソールに表示します。特定のコードまたは関数が実行するのに費やした時間を測定するには、console.time(timeName) を使用することができます。この機能は、JavaScript コードのパフォーマンス改善を目指している場合には非常に役立ちます。リスト 9 に一例を記載します。
リスト 9. console.time() の例
var timeName = 'measuringTime';  
console.time(timeName); //start of the timer   
for(var i=0;i<1000;i++){  
//do something  
console.timeEnd(timeName);  //end of the timer

コンソールには、measuringTime:xxms という形式で結果が出力されます。

Dojo ウィジェットのパフォーマンス

このセクションでは、Dojo ウィジェットのパフォーマンスを分析し、改善する方法を検討します。

ロードのコスト

Improving performance of Dojo-based web applications」(E. Lazutkin 著、2007年 2月) で指摘しているように、ほとんどのユーザーが Dojo に対して受ける第一印象は、そのサイズが膨大であることです。例えば、dojo-release-1.5.0-src.zip は 19M で、圧縮パッケージ・バージョンの dojo-release-1.5.0.zip でもサイズは 4.5M もあります。最小限のビルドに含まれるファイルでさえも、その大多数は冗長で、利用される見込みはありません。すべての Dojo ビルドには全 Dojo ファイルの完全なコピーと、最も頻繁に使われるファイルを組み合わせてカスタマイズされた dojo.js ファイルが付属しています。したがって、ロードのコストを小さくするための最善の方法は、適切な Dojo ビルドを使用することです。

dojo.js は Dojo オブジェクトを起動し、dojo.js のオプション部分によってすでにロードされているのでなければ、残りのモジュールをロードします。ブラウザーが dojo.js ファイルを初めてロードするときには、以下のファイルがアップロードされてセットアップされることになります。

  • Dojo ブートストラップ・コード
  • Dojo ローダー
  • 頻繁に使われるモジュール

ロード時間を短縮するには、どのビルドがアプリケーションにふさわしいかを検討してください。そうでないと、カスタム Dojo ビルドが必要となる羽目になります。Dojo の資料に関する詳細は、「参考文献」セクションを参照してください。

構文解析のコスト

Dojo ウィジェットの構文解析のコストを最小限に抑えるには、以下のいずれかの方法を使用して初期化部分を最適化してください。

タグのインスタンス化
HTML タグで Dojo ウィジェットを作成するには、dojoType 属性を追加します (リスト 10 を参照)。この方法が機能する前提として、dojo.parser が dojo.require("dojo.parser"); に組み込まれていて、djConfig="parseOnLoad:true" が設定されていなければなりません。この属性は、軽量のコードでコンポーネントを宣言する簡単な手段となります。ページ上で dojoType 属性を持つすべてのタグは、文書がロードされた後に自動的に構文解析されることになります。小さなアプリケーションには非常に便利な方法ですが、その一方、Web アプリケーションが HTML を多用しているとしたら、アプリケーションの起動時間が顕著に長くなります。それは、パーサーがすべての要素にアクセスして、その要素を構文解析する必要があるかどうかをチェックするためです。そのようなアプリケーションでは、例えば Firebug で提供しているプロファイルを使用して実行時間を調べてください。

dj_load_init() や modulesLoaded()、あるいは初期ロードのようなメソッドに時間がかかっていることが判明した場合には、ウィジェットの初期化部分について再検討する必要があります。

リスト 10. dojoType を使用した Dojo ウィジェットの作成
id="errorDialog" dojoType="dijit.Dialog" title='dialog1' class="pop-window"/>
コードのインスタンス化
Dojo は OOP フレームワークです。したがって、new を使用してウィジェットを作成することができます。ウィジェットを作成するには 2 つのパラメーターを入力しなければなりません。1 つは属性を持つ JSON オブジェクト、もう 1 つは構文解析する要素です。したがって、すべてのウィジェットには少なくとも 2 つのセンテンスが必要となります。一例をリスト 11 に記載します。
リスト 11. JavaScript の new を使用した Dojo ウィジェットの作成
new dijit.Dialog({"title":"dialog1 "},dojo.byId("dialog1"));

構文解析パフォーマンスの向上

コードの構造とパフォーマンスを最適化するには、ウィジェットを作成する時点で、構文解析のパフォーマンスを向上させることを検討してください。parseWidgets を false に設定した上で、要素の ID を設定するための配列を作成することで、自動構文解析を無効にします (リスト 12)。あるいは、実行時に新しい要素の ID を動的にプッシュすることもできます。そして文書がロードされるときに、dojo.forEach() によって、配列のすべての要素に含まれる ID を持つ要素について構文解析を行います。

リスト 12. 反復 searchIds によるウィジェットの構文解析
<head>
....
<script > djConfig = { parseWidgets: false, searchIds: [..] }; </script>
....
</head>
<body onload='dojo.forEach(djConfig.searchIds, 
   function(id){dojo.parser.parse(dojo.byId(id));});'>
........
</body>

ソリューション

Dojo グリッドでのパフォーマンスの問題は、主に入出力操作、大量のデータ・アクセス、そしてブラウザーによるデータのレンダリングに関連します。Dojo グリッド・ウィジェットのパフォーマンスは、いくつかのメカニズムをグリッドの機能と組み合わせて使用することで、改善することができます。

キャッシュ・メカニズムをどのように使用しているかを見直してください。データをデータベースからローカルにロードするときに、そのデータを長期間メモリーに保持すると、サーバー・サイドのデータをリクエストする際に応答時間を短縮する効果的な方法となります。ユーザーがグリッド内のデータを更新または変更するまでは、サーバー・サイドにリクエストを送信することがなくなるためです。キャッシュ・メカニズムは Dojo グリッド自体が大体の形で実装していますが、ユーザーがグリッド上で特定の操作を行うときに問題が持ち上がってきます。以下のシナリオで、そのような問題について説明します。

グリッドのソート
大抵の場合、グリッドは正しくソートすることができます。それは、グリッド自体がデータ・ストアのレベルでソート関数を実装しているためです。けれどもデータ・ストアはキャッシュを反映します。そのため、グリッドの列タイプが例えば中国語の文字だとすると、ソートの実行結果は正しいものにならず、不確定な要因によってグリッドのパフォーマンスが劣化することになります。

この問題に対するソリューションは、データ・ストアのレベルでソート関数を定義し直すことです。リスト 13リスト 14 に、その方法を示します。それは、onHeaderCellMouseDown 関数を基にソート・ロジックを作成し直して、データをレンダリングし、グリッドのヘッダー・ビューを更新するという方法です。

リスト 13. ソート・ロジックを再定義する
grid.onHeaderCellMouseDown = function(event){
	var items = DataStore.data.items;
//Sort the "Name" column which might contain characters like Chinese and so on
	if (event.cellIndex == 1) {
		sortAscending = ! sortAscending ;
//Change the string to localestring before comparison with localeCompare method
		if (sortAscending) {
			items.sort(function(m, n){
				return m["name"].toString().
				localeCompare(n["name"].toString());
			});
		}else {
			items.sort(function(m, n){
				return n["name"].toString().
				localeCompare(m["name"].toString());
			});
		}
	}else 
	//Sort the "Date" column
	if (event.cellIndex == 2) {
		sortAscending = !sortAscending;
		//Compare the date with milliseconds computed from 1970/07/01
		if (sortAscending) {
			items.sort(function(m, n){
			    return  Date.parse(m["date"].toString())
				 - Date.parse(n["date"].toString());
			});
		}else {
			items.sort(function(m, n){
				return Date.parse(n["date"].toString()) 
				- Date.parse(m["date"].toString());
			});
		}
	}
}
リスト 14. データをレンダリングし、グリッドのヘッダー・ビューを更新する
//"sorColIdx" is the index of the column to be sorted 
updateGridAfterSort : function(sortColIdx){
	//Render the data of the grid
	var store = new dojo.data.ItemFileWriteStore(DataStore.data);
	grid.setStore(store, null, null);
	grid.update();
	//Update the header view of the gird
	var headerNodeObjs = document.getElementsByTagName("th");
	for (var i = 0; i < 2; ++i) {
//"gridLayout" is a global array defining the layout of the grid
		var headerNodeObjName = gridLayout[0].cells[0][i].name;
		var headerNodeHtml = ['<div class="dojoxGridSortNode'];
		if (i == sortColIdx){
			headerNodeHtml = headerNodeHtml.concat([' ', (sortAscending ==
			 true) ? 'dojoxGridSortUp' : 'dojoxGridSortDown', '"><div 
			 class="dojoxGridArrowButtonChar">', (sortAscending == true) ? 
			 '▲' : '▼', '</div ><div class="dojoxGridArrowButtonNode"
			  ></div >']);
			headerNodeHtml = headerNodeHtml.concat([headerNodeObjName, 
			'</div>']);
			headerNodeObjs[i].innerHTML = headerNodeHtml.join(" ");
			break;
		}
	}
}
グリッドの検索
グリッドがリアルタイム検索やあいまい一致機能をサポートする場合はなおさらのこと、グリッドに大量のデータがあると、検索関数がパフォーマンスの劣化を招きます。これに対するソリューションは、データがデータ・ストアで使用される JSON オブジェクトに変換される前に、余っているメモリー・スペースを利用してデータをキャッシュすることです。こうすれば、多くの関数呼び出し (データ・ストアの getItem など) を防ぐことになります。リスト 15 に一例を記載します。
リスト 15. データベースからフェッチしたデータを配列にキャッシュしてから検索する
//Fetch data from database
getData : function() {
	function callback(ResultSet) {
//ResultSet is an array of records fetched from database and make variable
//rawData refer to it to cache it in memory  
		GlobalVaribles.rawData = ResultSet;
//Convert the raw data ResultSet to JSON for data store use 	
		GlobalVaribles.dataJSON = JSONUtil.convert2JSON(ResultSet);
	}
	DBUtil.fetchAll(callback);
}
//Search functionality
search: function(col, value){
	if (value == null || value == "") {
		return;
	}
//Used here
	var rawData = GlobalVaribles.rawData;
	var newResults = [];
	for (var i = 0; i < rawData.length; i++) {
		var result = rawData[i];
//Fuzzy match
		if(result[col].toLowerCase().indexOf(value.toLowerCase()) != -1){
			newResults[newResults.length] = result;
		}
	}
//Render the new results
	GridManager.renderNewResults(newResults);
}
遅延ロード・メカニズム
Dojo グリッドは、パフォーマンスを改善してユーザー・エクスペリエンスを快適なものにするための遅延ロード・メカニズムをサポートするように設計されています。Dojo グリッドでの遅延ロードは、データ・ストアに含まれるすべてのデータではなく、その一部だけをレンダリングすることを意味します。ユーザーがスクロール・バーをドラッグするまで、グリッドは残りのデータを表示しません。

デフォルトでは、グリッドは遅延ロード・メカニズムを開始しないため、このメカニズムを開始するように明示的に指示する必要があります。リスト 16 に、遅延ロード・メカニズムを開始する 2 通りの方法を示します。ここで重要になるコンポーネントは、rowsPerPage 属性と keepRows 属性です。

リスト 16. 遅延ロード・メカニズムを開始する
	            //The programmatic way
	            var grid = new dojox.grid.DataGrid({
	            store: store, //data store
	structure: gridLayout, 
	rowsPerPage: 10,  //Render 10 rows every time
	keepRows: 50,		//Keep 50 rows in rendering cache
}, "grid");   
//The declarative way using HTML label
<table  
dojoType="dojox.grid.DataGrid"
	id="grid" store="store" structure="gridLayout"
	query="{ id: '*' }" 
	rowsPerPage="10" keepRows="50">
	<!--  other definitions  -->
</table>
事前ロード・メカニズム
事前ロード・メカニズムは、ユーザーが一時的にしか必要としないとしても、表示域に入っていない残りのデータを前もってロードします。Dojo グリッドの場合、データ・ストアには大量のデータがある可能性があります。ユーザーが表示域外のデータを表示するには、スクロール・バーをドラッグします。1 つのページに大量のデータがあるとしたら、1 つの特定の行だけを表示するのでは利便性に欠けます。その場合には事前ロード・メカニズムとページング手法を使用することで、ビューの利便性が上がり (Google のページング・バーと同様)、パフォーマンスが改善されます。

リスト 17 に、ページング手法を使った基本的な実装を記載します。このコードではまず、データ・ストアが初期ページング・バーに使用するいくつかの JSON オブジェクトを作成し、ユーザーがページング・バーで最後のページをクリックすると、新しい JSON オブジェクトが動的に追加されるようにしています。

リスト 17. 最初に JSON オブジェクトを作成し、必要に応じてオブジェクトを切り替える
//Fetch data from database and convert them to JSON objects
getData : function() {
	function callback(ResultSet) {
		GlobalVaribles.rawData = ResultSet;
//"convert2JSONs" method convert the raw data to several JSON objects
//stored in Global array "dataJSONs".
		GlobalVaribles.dataJSONs = JSONUtil.convert2JSONs(ResultSet);
	}
	DBUtil.fetchAll(callback);
}
//Initial status 
var dataStore = new dojo.data.ItemFileWriteStore({data:GlobalVaribles.dataJSONs[0]});
var grid = new dojox.grid.DataGrid({
	store: dataStore ,
	structure: gridLayout,
}, "grid");
grid.startup();
//Assuming that the user clicks the i-th item in the paging bar, we just update the data  
//store for the grid simply and the performance is still very good.
dataStore = new dojo.data.ItemFileWriteStore({data:GlobalVaribles.dataJSONs[i-1]});
grid.setStore(dataStore, null, null);
grid.update();

まとめ

この記事では、Web アプリケーションに存在するさまざまな問題、つまりボトルネックを特定する方法を説明しました。記事で説明したツール、秘訣、そしてアドバイスを利用して、ユーザーのためにパフォーマンスを調整して向上させてください。


ダウンロード可能なリソース


関連トピック


コメント

コメントを登録するにはサインインあるいは登録してください。

static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=60
Zone=Web development
ArticleID=551220
ArticleTitle=Web アプリケーションのパフォーマンスを改善する
publish-date=09212010