LoginSignup
6
4

More than 1 year has passed since last update.

要素数10万のHTMLページを使ってCSSセレクターの照合にかかる時間を測定した

Posted at

querySelectorAll()を使ってCSSセレクターのパフォーマンスを測定した結果を紹介した記事があったので、自分でもやってみました。

実行環境

  • Chrome 96
  • macOS Big Sur 11.6.2
  • iMac 2017 / CPU 3.5GHz quad-core Intel Core i5 / メモリ32GB

参考にしたのはこの記事とコードです。

[資料1] Optimizing CSS: ID Selectors and Other Myths
[資料2] CodePen: CSS selector performance

CSSセレクターのパフォーマンスの観点で私が以前から気になっていたのが、たとえば以下のようなマークアップでa要素にスタイルをあてる場合です。

html
<ul>
  <li>The quick <a href="#">brown fox</a> jumps over the lazy dog</li>
</ul>

要素の親子関係を使って「このa要素」を表すCSSセレクターを書く場合、ul, li, a要素にclassを割り当ててあるか・いないかによって選択肢がいくつもあります。たとえばul要素だけclassを割り当てて、.list li a.list aと書くなど。一方、ブラウザがセレクターを解釈する処理は右から左へ行われるらしいので、このようなセレクターの形式は要素の照合に時間がかかってパフォーマンスが低下してしまうのでは?と気になったりもします。そこで、セレクターの書き方によって要素の照合にかかる時間がどれだけ増えるのか、要素数10万のHTMLページを作って実際に測って確かめてみることにしました。

結論から言うと、li要素の子のa要素については、10万要素1のHTMLページであってもセレクターの書き方の違いによるパフォーマンスの差は数ミリ秒程度のものでした。

測定方法

テスト用のHTMLページ

テスト用のページは以下の構造を持つdiv.boxを10,000個配置したもので、HTML要素数は全体で約100,000個になります。ここで${count}は各div.boxに対して割り当てた連番(1〜10,000)が入ります。

html
<div class="box b${count}">
  <div class="title"><p>${count}</p></div>
  <ul class="list">
    <li class="item first"><a href="#" class="link">_</a></li>
    <li class="item active"><a href="#" class="link">_</a></li>
    <li class="item last"><a href="#" class="link">_</a></li>
  </ul>
</div>

実際のページはこれです。資料2のコードからforkして今回の測定用に改造しました。

Part 1: https://codepen.io/kaz_hashimoto/pen/rNGGKJj

Part 2: https://codepen.io/kaz_hashimoto/pen/rNGYrvJ

画面のMeasureボタンをクリックすると測定を開始し、DevToolsのコンソール画面に結果を表形式で出力します。測定は、querySelectorAll()の呼び出し前後のタイムスタンプの差の平均値(単位ミリ秒)です。試行回数はPart 1が20回、Part 2が30回です。

javascript
function test(selector) {
  const t0 = performance.now();
  document.querySelectorAll(selector);
  const t1 = performance.now();
  const msec = t1 - t0;
  return msec;
}

テスト項目と測定結果

今回テストした項目と測定結果の要約は下表のとおりです。「li要素の子のa要素」に対するセレクター(項目No.1)に加えて、パフォーマンスの観点から気になっていた項目(No.2〜8)も追加しました。

テストページPart 1

# 概要 結果
1 ul>li>a要素を選択する 3.1〜5.1ms
2 擬似クラスを付けてul>li>a要素を選択する 4.4〜4.8ms
3 兄弟結合子を使ってdivを選択する 2.5〜2.6ms
ただし、特定のパターンで596ms

テストページPart 2

# 概要 結果
4 多数のclassを持つ要素に対してclass名で選択する 5.6〜6.8ms
5 長いclass名を持つ要素に対してclass名で選択する 3.1〜4.6ms
6 孤立したclass名をセレクターの前に付けて選択する 1.6〜4ms
7 孤立したclass名をセレクターの前に付けて選択する(#6 + #4) 6.6〜9.4ms
8 孤立したclass名をセレクターの前に付けて選択する(#6 + #5) 3.7〜6.2ms

※「孤立したclass名」とはHTMLページのどの要素からも参照されないclass名のことです。

結果の詳細

Test#1: ul>li>a要素を選択する

セレクターの組み合わせパターン

Test#1で対象とする「li要素の子のa要素」を表すセレクターのパターンについては、以下の5つの項目の組み合わせで構成されるセレクターとしました。他にもIDセレクターや全称セレクターが含まれるケースも考えられますが、キリがないので除外しました。

  1. 要素を直接指定: ul, li, a
  2. class名を指定: .list, .item, .link
  3. 要素とclass名を指定: ul.list, li.item, a.link
  4. 子孫結合子(スペース)を指定: ul li a, .item aなど
  5. 子結合子「>」を指定: ul > li > a, .item > aなど

querySelector()の戻り値がundefinedになるものを除外すると、有効なセレクターは全部で138個になりました(組み合わせを生成するソースコードが正しければ…)。

測定結果

138個のセレクターについての測定結果のconsole出力を下図に示します。行の順序はtime値(ミリ秒)の昇順です。specificityはセレクターの詳細度です。

まずは速い方から。

速いセレクター上位20
selector-1_fix.png
a要素にclassを付けない場合は、祖先のul要素とのペアよりも親要素liと組み合わせたパターンにする方がパフォーマンスが若干良いようです。

遅いセレクター下位20
selector-2_fix.png
パターンが複雑になり詳細度の下2桁が大きいセレクターでは、プラス1〜2ms処理時間が増えてきました。ul.listのように「要素名.class名」と言う形式は詳細度が増えて扱いづらいだけでなく、パフォーマンス的にも不利なのがよくわかりました。

Test#2: 擬似クラスを付けてul>li>a要素を選択する

次に、li要素の兄弟要素のグループ内での位置を指定してa要素を選択するケースです。liに擬似クラス:nth-child(), :first-child, および:last-childのいずれかを付けた場合と、位置を表すclass名で代用した場合とを比較しました。

html
 <ul class="list">
  <li class="item first"><a href="#" class="link"></a></li>
  <li class="item active"><a href="#" class="link"></a></li>
  <li class="item last"><a href="#" class="link"></a></li>
</ul>

selector-3_fix.png
class名で代用したセレクターと比較して、擬似クラスを用いたセレクターは相対的にやや速度が落ちるようです。

Test#3: 兄弟結合子を使ってdivを選択する

次は、10,000個のdiv.boxに対して、隣接兄弟結合子(+)および一般兄弟結合子(~)を使ったセレクターで要素を選択した場合です。div.box要素には連番でそれぞれ一意のclass名b1, b2, ...を振ってあります(下図)。
html-2_50p.png
selector-4_fix.png
先行要素に".b5000"を持つ後続要素".box"を選択するセレクター".b5000 ~ .box"だけが非常に遅く、約596msもかかっています。

Test#4: 多数のclassを持つ要素に対してclass名で選択する

次は、各要素にclassを30個付けた状態でそのうちの1つのclassをセレクターに指定した場合です。
selector-31_fix.png
Before: 30個のclassを付ける前の状態でテスト
まずは比較のため、classが1〜2個しか付いていない初期状態で実行した時の結果です。
selector-23_fix.png
After: classを30個付けた状態でテスト
こちらは要素にclassを30個付けて、セレクターには15番目のclass名を指定して要素を選択した時の結果です。
selector-21_fix.png
要素のみのシンプルなセレクター(div, ul, li, a)はBefore/After共に2ms程度で増加も0.7ms以下なのに対して、class名を指定した方は3〜4ms遅くなっています。

Test#5: 長いclass名を持つ要素に対してclass名で選択する

次は、各要素に30文字程度の長いclass名を付けてそのclass名をセレクターに指定した場合です。
selector-32_fix.png
selector-22_fix.png
初期状態(Test#4 Beforeの表)よりはスピードが落ちますが、classが30個のケース(Test#4)ほどには低下しません。

Test#6: 孤立したclass名をセレクターの前に付けて選択する

Test#6〜#8は、どのHTML要素からも参照されていないclass名".notdef"をセレクターの前に付けた場合です。以下のような事例を想定しました。

  • 「処理の状態」や「機能の有無」を表す文字列をJavaScriptを使ってbody要素のclassに追加しているページ
  • CSSファイルを流用したため、そのHTMLページで使われていないCSSルールが大量にある。

まずはシンプルなセレクターに".notdef"を付けた場合。
selector-23a_fix.png
Test#4 Beforeの表と比べて、セレクターのスピードに目立った低下はありません。

Test#7: 孤立したclass名をセレクターの前に付けて選択する(#6 + #4)

次に、要素が多数のclassを持っているページで(Test#4)、".notdef"をセレクターの前に付けた場合です。
selector-21a_fix.png
シンプルなセレクターも同様にTest#6の結果と比べて4〜5ms遅くなっています。

Test#8: 孤立したclass名をセレクターの前に付けて選択する(#6 + #5)

最後は、要素が長いclass名を持っているページで(Test#5) 、".notdef"をセレクターの前に付けた場合です。
selector-22a_fix.png
シンプルなセレクターも同様にTest#6の結果と比べて1ms程度遅くなっています。

JSライブラリ

測定結果に出力されるセレクターの詳細度については、以下のライブラリを使用しました。

Specificity Calculator 0.4.1
GitHub: https://github.com/keeganstreet/specificity
CDN: https://unpkg.com/specificity@0.4.1/dist/specificity.js


  1. ちなみに某ニュースサイトのページでbody要素の下のノード数をquerySelectorAll()でカウントすると3000〜3500個くらいです。 

6
4
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
6
4