pixiv insideは移転しました! ≫ https://inside.pixiv.blog/

フロントエンドで知っておきたい要素指定の考え方

みなさんはじめまして、ピクシブのフロントエンドエンジニア id:koharusugiura です。

JavaScript を用いた開発を行う際に jQueryReactvue.js といったライブラリーを使う方は多いでしょう。これらのライブラリーは共通して DOM を扱うライブラリーとなります。

DOM についての説明は長くなるのでここでは省きますが、簡単に説明すると HTML や XML の構造を表現するための仕様です。DOM は JavaScript のためだけにある仕様ではなく、 Java や Python など、多くの言語に対応する仕様です。

前述した各ライブラリーは多くのウェブブラウザー間の差異を吸収してくれるため、非常に便利なものです。しかし、その機能の豊富さからライブラリー自体のサイズは大きく、ウェブブラウザーで読み込むファイルの数や合計の容量が増えてしまいます。状況にもよりますが、軽量に済ませたい場合はライブラリーを使わずに DOM を直接扱うと良いでしょう。

そこで今回は、DOM の基本的なところについて書きます。

この記事はピクシブ株式会社 Advent Calendar 2015 23 日目の記事です。

qiita.com

※ この記事でサンプルとして記載しているコードは特筆のない限り ECMAScript 3.1 に準拠しています。

ID から要素を取得

DOM で HTML から任意の ID を付与された要素の取得を行う場合は以下のように記述します。

// DOM Core
var element = document.getElementById('element-id');
// Selectors API
var element = document.querySelector('#element-id');

通常の使用で速度の差が影響する場面はほとんどありませんが、DOM Level 2 Core で規定されている getElementById メソッドの方が高速に動作します。これは Selectors API が汎用的に要素の取得を行うための仕様であり、ID の取得に特化していないためです。

タグ名から要素を取得

HTML に含まれる任意のタグ名の要素を複数取得し、それぞれの要素に対して特定の処理を行う場合は以下のように記述します。

// DOM Level 2 Core
var anchors = document.getElementsByTagName('a');
var anchorsLength = anchors.length;
var i, anchor;
for (i = 0; i < anchorsLength; ++i) {
  anchor = anchors[i];
  doSomething(anchor);
}
// Selectors API
var anchors = document.querySelectorAll('a');
var anchorsLength = anchors.length;
var i, anchor;
for (i = 0; i < anchorsLength; ++i) {
  anchor = anchors[i];
  doSomething(anchor);
}

前述の getElementById メソッドと同様に、DOM Level 2 Core で規定されている getElementsByTagName メソッドで複数要素の取得し、ループで各要素を処理したものの方が高速に動くと思われるかもしれません。ですが、実際には多くのウェブブラウザーで Selectors API を使って要素を取得し、ループで各要素を処理したものの方が高速に処理されます。

これは getElementsByTagName メソッドが動的な HTMLCollection オブジェクトを返すのに対し、Selectors API の querySelectorAll メソッド が静的な NodeList オブジェクトを返すためです。

具体的な問題にぶつかることは珍しいため、両メソッドが返すオブジェクトの違いを意識している人はまずいないでしょう。そのため、動的なオブジェクトや静的なオブジェクトと言われてもイメージがわかない人がほとんどかと思います。

以下の HTML 文書をウェブブラウザーで開くと、result: false と表示されるでしょう。

<!doctype html>
<title>HTMLCollection !== NodeList</title>
<p><a href="http://example.com/" id="anchor">example</a></p>
<p>result: <output id="result"></output></p>
<script>
var anchorsFromDomCore = document.getElementsByTagName('a');
var anchorsFromSelectors = document.querySelectorAll('a');
var anchor = document.getElementById('anchor');
anchor.parentNode.removeChild(anchor);
document.getElementById('result').value = anchorsFromDomCore.length === anchorsFromSelectors.length;
</script>

HTMLCollection オブジェクトは取得された HTML 文書に対する変更に追随します。そのため、取得した要素が削除された場合、HTMLCollection オブジェクトからも削除されます。一方 NodeList オブジェクトでは取得時の状態が保持され、一度取得された要素は削除されません。これが動的なオブジェクトと静的なオブジェクトの違いです。

HTML 文書に対する変更を追うため、HTMLCollection オブジェクトを返す getElementsByTagName メソッドを for 文でループさせる処理は遅くなるのです。

では、getElementsByTagName メソッドを使わず、常に Selectors API で要素の取得を行えば良いのかと言えばそうではありません。

ID から要素を取得する getElementById メソッドと同じく、Selectors API での要素の取得より getElementsByTagName メソッドの方が高速です。これは取得する要素の数が増える度、顕著になります。getElementsByTagName メソッドが遅いのはあくまでループで処理を回す時のみです。

さて、HTMLCollection オブジェクトと NodeList オブジェクトの両オブジェクトはいわゆる「Array like object」です。Array like object とは length プロパティーを持ち、数値のキーを持つオブジェクトのことを差します。Array オブジェクトを継承しているのではなく、あくまで Array オブジェクトと似たオブジェクトです。

DOM の観点から言えば Array オブジェクトは静的なオブジェクトです。HTMLCollection オブジェクトから Array オブジェクトに変換すると Selectors API よりも高速にループの処理ができます。

HTMLCollection オブジェクトから Array オブジェクトへの変換には Array.prototype.slice メソッドを使います。要素の数にもよりますが、Array.prototype.slice での処理には相応の処理速度を必要とします。しかし、HTMLCollection オブジェクトのループ処理や Selectors API を用いた要素の取得と比較すれば無視できる程度です。

var anchors = Array.prototype.slice.call(document.getElementsByTagName('a'));
var anchorsLength = anchors.length;
var i, anchor;
for (i = 0; i < anchorsLength; ++i) {
  anchor = anchors[i];
  doSomething(anchor);
}

クラス名から要素を取得

HTML に含まれる任意のクラス名の要素を複数取得し、それぞれの要素に対して特定の処理を行う場合は getElementsByClassName メソッドを使います。Selectors API でも取得できますが、getElementById メソッドや getElementsByTagName メソッドとの比較と同様に getElementsByClassName のほうが高速なため、ここでは省きます。

// W3C DOM 4
var elements = Array.prototype.slice.call(document.getElementsByClassName('element-class-name'));
var elementsLength = elements.length;
var i, element;
for (i = 0; i < elementsLength; ++i) {
  element = elements[i];
  doSomething(element);
}

getElementsByClassName メソッドは getElementsByTagName メソッドと同様に HTMLCollection オブジェクトを返します。そのため、getElementsByTagName メソッドの場合と同じく、Array.prototype.slice メソッドを使い Array オブジェクトに変換してから処理します。

getElementsByClassName メソッドは getElementById メソッドや getElementsByTagName メソッドと比べると新しい仕様です。しかし、ほかのウェブブラウザーよりも実装の遅かった Internet Explorer でも、2011 年春に公開された Internet Explorer 9 の時点で実装されています。そのため、Internet Explorer 8 以前のウェブブラウザーに対応する必要がなければ気にせず使えます。

最後に

jQuery は上記の処理を内部的に行います。

引数に与えられたセレクターを正規表現でパースし、ID からの取得であれば getElementById に渡し、タグ名からの取得であれば getElementsByTagName に渡し、クラス名からの取得であれば getElementsByClassName に渡します。そのいずれでもなく、かつ jQuery 独自の疑似要素の指定がない場合のみ Selectors API にセレクターの解釈をさせます。

そのため、Selectors API をどのような要素の取得方法に対しても使うと jQuery のほうが高速になります。しかし、要素の取得方法に応じて適切なメソッドを選択すると、jQuery の内部で行われる正規表現の処理や if 分岐を省けるため、jQuery を使うよりも高速になります。

また、jQuery 自体を読み込むのにかかる時間もあります。処理の内容にもよりますが、jQuery を省けるのであれば省いてしまいましょう。

高速な処理こそ、最適なユーザー体験につながります。

次は Microsoft MVP for Internet Explorer に認定され、また東京 Web Performance を主催する弊社のフロントエンドエンジニアの雄、furoshiki によるイカした記事になります。みなさまご期待くださいませ。