7. JavaScriptの開発方法

jQueryを利用したクライアントサイドの開発では、必然的にJavaScriptを効率よく開発することが重要となる。
JavaScriptエンジンの発展や標準化によってブラウザ間での挙動の差異は少なくなり、またjQueryをはじめとするライブラリの導入が当たり前になったことで、少ないコード量で画面を開発することが可能となった。
一方で、クライアントマシンの性能向上やネットワーク環境の向上により、より複雑でリッチな画面を開発することも多くなったため、開発するコード量は以前よりも多く、また難易度も上がってきている。
複雑でリッチな画面を開発することで、様々な問題が発生することが想定される。
複数メンバで開発する場合には、メンバ間のスキルのばらつきがあり、統制が取れなかったことが原因で、生産性や品質、保守性の低下を招くことがある。
また、複雑でリッチな画面の場合には、画面描画が遅くなったり性能が問題になることもある。
この章では、ガイドラインで紹介しているライブラリを用いて画面を開発する際に、問題の発生を未然に防ぐ方法や、問題が発生した際にどういうアプローチで解決すればよいか、またどういった解決策があるかを紹介する。

7.1. JavaScriptの性能チューニング

クライアントサイドの開発では、以前からJavaScriptの性能が問題となることが多かった。
近年はブラウザのJavaScriptエンジンの高速化によって改善されてきているが、処理コストが大きかったり、無駄があるコードが原因となり、体感できるほどの性能問題を引き起こす事もある。
JavaScriptの性能問題は、ユーザビリティの低下を招くことにも繋がることがあるため、コードを見直し、処理性能の改善に取り組むべきである。
一つ一つの改善効果は小さいかもしれないが、積み重ねることが重要である。
ここでは、JavaScriptの性能チューニングの参考となるコーディングについて、説明する。
ガイドラインで紹介しているUIコンポーネントを組み合わせて複雑な画面を作成する際に性能問題が起きた場合は、ここに記載されている事項を確認し、より効率的な処理への変更や実現したい要件に適した実装へ見直すことで問題が解決するかもしれない。
また、設計やコーディングを行う前に事前に注意点を確認した上で作業することで、性能問題の発生を未然に防ぐことに努めてほしい。

Note

性能問題が発生した場合、ChromeのデベロッパーツールのTimelineやProfile、Firefoxの開発ツールのパフォーマンス、IEの開発者ツールのプロファイラーなどを利用すると、どのメソッドで時間がかかったのか調査しやすい。

またChromeのデベロッパーツールの場合、コード内に以下のメソッドを記述することで、その区間のプロファイルを取得する事ができる。

profileメソッドにtitleを設定することで、同じtitleのprofileEndメソッドまでの間のみを解析してくれるため、複数の区間に分けて取得する事もできる。

console.profile('title');
console.profileEnd('title');

性能改善は、下記の大まかなフローで実施する。

  • ツールを用いて処理時間の調査を行うことで、どのメソッドにどれだけ時間がかかったのかを明らかにする。
  • 処理時間のかかっているメソッドを確認し、改善すべき処理の絞り込みを行う。
  • 改善すべき処理に対して、対策を実施する。

7.1.1. JavaScriptの処理と性能

ここでは、JavaScriptの基本的な記述方法について、性能改善のためのポイントを説明する。

7.1.1.1. キャッシュの利用

JavaScript では、ローカル変数のアクセスが最も速く、スコープチェーンの外側にある変数へのアクセスほど遅くなる。このため、スコープチェーンの一番外側にあるグローバル変数へのアクセスが最も遅い。
よって、頻繁にアクセスが必要なグローバル変数は、可能な限りローカル変数にキャッシュした方が良い。
以下に例を示す。
var foo;

// (1)
var fooTest = function () {
  foo = 'foo';
  foo += ' test';
  return foo;
};

// (2)
var barTest = function () {
  var bar;
  bar = 'bar';
  bar += ' test';
  return bar;
};

// (3)
var bizTest = function () {
  var biz = foo;
  biz = 'biz';
  biz += ' test';
  return biz;
};
項番 説明
(1)
グローバル変数へのアクセスは遅い。
(2)
ローカル変数へのアクセスは速い。
(3)
グローバル変数はローカル変数にキャッシュすることで、高速にアクセスできる。
上記コードでは利用回数が少ないのであまり差は出ないが、ループ内で利用される変数や頻繁に利用される変数はローカル変数にキャッシュした方が良い。

7.1.1.2. 配列やオブジェクトの参照を利用した条件判定

一般的に条件判定にはif文やswitch文が使われるが、数値や文字列と言った特定のキーに対して固定の値を返すような処理の場合、候補となる項目数が増えるほど生産性や保守性の面で効率が悪い。
そのような処理の場合、配列やオブジェクトの参照を利用すればキーから値を直接返すことができるので効率が良い。
また、項目数が膨大であるほど性能面で優れている。
var value1 = 3;
var value2 = 'c';

// (1)
var data1 = ['result 0', 'result 1', 'result 2', 'result 3',
             'result 4', 'result 5', 'result 6', 'result 7'];
var result1 = data1[value1] || 'no result';

// (2)
var data2 = {a:'result a' ,b:'result b', c:'result c', d:'result d'};
var result2 = data2[value2] || 'no result';
項番 説明
(1)
配列を利用した例。一致する項目がない場合はundefinedととなる。
(2)
オブジェクトを利用した例。一致する項目がない場合はundefinedととなる。

7.1.1.3. ループ処理の最適化

ループでは同じ記述が何度も利用されるため、一層の配慮が必要となる。また、ループ終了条件の判定で、よくi < list.lengthという使い方がされているが、以下のように、ループの前でローカル変数にキャッシュする対処をすべきである。
var i;
var list = ['a', 'b', 'c', 'd'];

// (1)
for (i = 0; i < list.length; i++) {
  resultString(list[i]);
}

// (2)
var length = list.length;
for (i = 0; i < length; i++) {
  resultString(list[i]);
}
項番 説明
(1)
よく利用されるfor文。ループ回数分、.lengthの取得処理が実行されてしまう。
(2)
list.lengthをローカル変数にキャッシュする事で、.lengthの取得処理は1回のみとなる。
このような対応はfor文だけでなく、do-while文、while文でも同様に有効である。
ループ条件だけでなく、ループ内の処理にも注意すること。
jQueryオブジェクトやDOMの取得、再描画が発生するDOM操作といったような、性能に影響する処理は可能な限りループの外で行ったほうが良い。

Note

for文、do-while文、while文のループの性能はどれも同程度となる。

しかし、for-in文はオブジェクトの列挙可能なプロパティを全て探しだすため、上記の3つと異なり性能は大きく劣る。

そのため、for-in文を利用するのは、オブジェクトのプロパティを扱うときのみ利用する。

また、for-in文は順番が規定されていないため、ブラウザによって処理する順序が異なってしまう可能性がある点も注意すること。

7.1.1.4. 画面再描画回数の削減

JavaScriptで要素に子要素を追加する、といったような、DOMが画面表示に影響する変更を受けると、画面の再描画が発生する。
再描画は変更された要素だけでなく画面全体で行われるため、その処理はとても重い。
再描画は以下のようなタイミングで起こる。
  • 画面の初期表示
  • 要素の追加や削除
  • 要素の位置変更
  • 要素のサイズ変更
  • テキストの変更
  • ブラウザのサイズ変更(スマートフォンやタブレットでの縦横回転)
そのため、再描画を極力減らすことが重要となる。
mainというidのdivの下に5つのdivを配置する場合を考える。
var main = document.getElementById('main');
var div1 = document.createElement('div');
var div2 = document.createElement('div');
var div3 = document.createElement('div');
var div4 = document.createElement('div');
var div5 = document.createElement('div');

// (1)
main.appendChild(div1);
main.appendChild(div2);
main.appendChild(div3);
main.appendChild(div4);
main.appendChild(div5);
項番 説明
(1)
そのままappendChildを実行した例。
mainappendChild()を5回実行すると、再描画も5回実行されてしまう。
このような場合に再描画を減らす方法としては、ドキュメントフラグメントを利用する。
元となるHTMLドキュメントとは独立したドキュメントフラグメントを作成し、ドキュメントフラグメントでDOMを構築して元となるHTMLドキュメントに追加する。
これにより、再描画はHTMLドキュメントに追加する1回のみとなる。
// (1)
var fragment = document.createDocumentFragment();
// (2)
fragment.appendChild(div1);
fragment.appendChild(div2);
fragment.appendChild(div3);
fragment.appendChild(div4);
fragment.appendChild(div5);
// (3)
main.appendChild(fragment);
項番 説明
(1)
ドキュメントフラグメントの作成。
(2)
ドキュメントフラグメントに子要素を追加する。
(3)
対象にドキュメントフラグメントを追加する。
ドキュメントフラグメントでは親要素にappendChild()した際に、自身の子要素を丸ごと親要素に渡すことができ、通常のappendChild()では出来ない、複数の子要素の受け渡しも可能になる。

7.1.2. jQuery利用時のチューニング

jQueryの特徴は、CSSセレクタを利用した簡単なDOMアクセスと、豊富なDOM操作機能である。
#id .classといったようなCSSセレクタの形式で指定することで、簡単に該当のDOMにアクセスすることができるため、コードをシンプルにすることができ、生産性や保守性の面でメリットとなっている。
セレクタで指定したDOMはjQueryオブジェクトと呼ばれるオブジェクトに変換されることで、様々なAPIが利用できる。
jQueryのDOMアクセスはそのままでも高度に最適化されているが、セレクタの記述方法によって性能に差が出る事もある。
jQueryを利用する際に注意すべきポイントについて以降で説明する。

7.1.2.1. シンプルなセレクタの利用

jQueryでセレクタを利用すると、jQueryの内部の手順は大きく分けて以下の3段階で処理される。
  1. ネイティブAPIのgetElementById()getElementsByTagName()getElementsByClassName()が利用できるのであれば使用する
  2. 1.に当てはまらない場合、CSSセレクタ形式として、querySelectorAll()を使用する
  3. 2.にも当てはまらない場合(CSSセレクタとして解析できない場合)、セレクターエンジンSizzleがセレクタを解析する
処理速度はネイティブAPIが高速で、その中でもgetElementById()が最速となる。
querySelectorAll()は若干低速で、Sizzleは他と比べるととても低速である。
このため、セレクタは可能な限り、id指定、タグ指定、クラス指定のいずれかのみのシンプルなセレクタが最も望ましい。
1つの指定で絞りきれないような条件の場合でも、CSSセレクタでの形式(querySelectorAll()でエラーにならない形式)が望ましい。
// (1)
$('#foo');

// (2)
$('.bar');

// (3)
$('span');

// (4)
$('#foo .bar');

// (5)
$('#foo:first');
項番 説明
(1)
最も高速なid指定。
(2)
高速なクラス指定。
(3)
高速なタグ指定。
(4)
若干低速なCSSセレクタ形式。
(5)
とても低速な拡張セレクタ利用。

7.1.2.2. jQuery独自の拡張セレクタを利用しない

jQueryではCSSセレクタ以外に、「:first」「:even」といったようなjQuery独自の拡張セレクタが利用できる。
しかし、これらのセレクタを利用するとセレクターエンジンSizzleでの処理となってしまうため、性能は落ちてしまう。このため、可能な限り拡張セレクタは利用せず、querySelectorAll()で解析可能な、CSSセレクタを利用すべきである。
// (1)
$('#foo li:even');

// (2)
$('#foo li:nth-child(odd)');
項番 説明
(1)
拡張セレクタである、:evenを利用した例。Sizzleが利用されるため、低速。
(2)
CSS3のセレクタで記述した例。querySelectorAll()で処理されるため、(1)よりも高速。

7.1.2.3. キャッシュの利用

jQueryオブジェクトの生成はコストがかかるため、複数回利用するjQueryオブジェクトはローカル変数へキャッシュしておくことで、生成する手間を省くことができる。
// (1)
$('#foo').addClass('bar');
$('#foo').removeClass('baz');
$('#foo').toggleClass('qux');
$('#foo').text('change text');

// (2)
var foo = $('#foo');
foo.addClass('bar');
foo.removeClass('baz');
foo.toggleClass('qux');
foo.text('change text');
項番 説明
(1)
毎回jQueryオブジェクトを生成すると処理コストがかかる。
(2)
生成したjQueryオブジェクトをキャッシュすることで、生成するのは一回だけとなるため、こちらの方が高速。
キャッシュすることで、実際に処理を実行するかの判定にも利用しやすい。
処理によってはセレクタから取得したjQueryオブジェクトに選択要素が含まれていなくてもオーバーヘッドが発生する事がある。
例として、.slideUp().slideDown()等のエフェクトAPIには、要素の存在チェックの前に、アニメーション処理の為の前処理が実行されてしまうため、それがオーバーヘッドとなる。
そのため、jQueryオブジェクトの.lengthが0であれば処理を行わない、といった分岐を作成する際に利用できる。
// (1)
$('#foo').slideUp();

// (2)
var foo = $('#foo');
if (foo.length) {
  foo.slideUp();
}
項番 説明
(1)
idがfooという要素がなくても処理が発生する。
(2)
事前に存在をチェックすることで、無駄な処理を発生させない。

Note

jQueryのセレクタで取得したjQueryオブジェクトに要素が存在するかチェックする場合、.lengthを利用すること。

var foo = $('#foo');

// (1)
if (foo) {
  foo.slideToggle();
}

// (2)
if (foo.length) {
  foo.slideToggle();
}
項番 説明
(1)
オブジェクト自体は存在しているため、当てはまる要素が存在しない場合でも、trueとして扱われてしまう。
(2)
.lengthは当てはまる要素の個数を返すので、存在しない場合は0を返し、falseとして扱われる。

7.1.2.4. メソッドチェーンの利用

あるjQueryオブジェクトに連続して処理を行う場合は、キャッシュの利用以外にもメソッドチェーンを利用する方法がある。
利用箇所がそのメソッドチェーンのみで済む場合、ローカル変数を増やす必要がなくなる。
// (1)
$('#foo').addClass('bar');
$('#foo').removeClass('baz');
$('#foo').toggleClass('qux');
$('#foo').text('change text');

// (2)
$('#foo')
    .addClass('bar')
    .removeClass('baz')
    .toggleClass('qux')
    .text('change text');
項番 説明
(1)
毎回jQueryオブジェクトを生成すると処理コストがかかる。
(2)
メソッドチェーンを利用することでjQueryオブジェクトの生成は一回だけとなる。

7.1.2.5. find()の利用

セレクタの条件が、idで対象の範囲を絞込み、別の条件で検索するようなパターンの場合、find()を利用すると速くなる場合がある。
ただし、DOMの構成やセレクタの条件によっては、通常のquerySelectorAll()で実行できる1回のセレクタ記述の方が速い場合がある。
// (1)
$('#foo span:hidden');

// (2)
$('#foo').find('span:hidden');
項番 説明
(1)
id指定と拡張セレクタの組み合わせ。
(2)
高速なid指定で絞り込んだ後に、低速な拡張セレクタで検索する。DOMの構成によっては高速。

7.1.2.6. JavaScriptのネイティブAPIを利用

jQueryのAPIを利用する必要がない場合は、getElementById()といったようなネイティブのAPIを利用した方が高速となる。
コードの統一性や可読性等の点からはセレクタを利用した方が良いが、どうしても性能を追求したい場合はネイティブAPIの利用も検討すること。
// (1)
$('#foo');
$('span');
$('.bar');
$('#foo .bar');

// (2)
document.getElementById('foo');
document.getElementsByTagName('span');
document.getElementsByClassName('bar');
document.querySelectorAll('#foo .bar');
項番 説明
(1)
jQueryオブジェクトを取得するためのid指定、タグ指定、クラス指定、CSSセレクタ指定の例。
(2)
ネイティブAPIでDOMを取得するためのid指定、タグ指定、クラス指定、CSSセレクタ指定の例。jQueryオブジェクトの取得よりも高速。