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要素にスタイルをあてる場合です。
<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)が入ります。
<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回です。
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セレクターや全称セレクターが含まれるケースも考えられますが、キリがないので除外しました。
- 要素を直接指定: ul, li, a
- class名を指定: .list, .item, .link
- 要素とclass名を指定: ul.list, li.item, a.link
- 子孫結合子(スペース)を指定: ul li a, .item aなど
- 子結合子「>」を指定: ul > li > a, .item > aなど
querySelector()の戻り値がundefinedになるものを除外すると、有効なセレクターは全部で138個になりました(組み合わせを生成するソースコードが正しければ…)。
測定結果
138個のセレクターについての測定結果のconsole出力を下図に示します。行の順序はtime値(ミリ秒)の昇順です。specificityはセレクターの詳細度です。
まずは速い方から。
速いセレクター上位20
a要素にclassを付けない場合は、祖先のul要素とのペアよりも親要素liと組み合わせたパターンにする方がパフォーマンスが若干良いようです。
遅いセレクター下位20
パターンが複雑になり詳細度の下2桁が大きいセレクターでは、プラス1〜2ms処理時間が増えてきました。ul.list
のように「要素名.class名」と言う形式は詳細度が増えて扱いづらいだけでなく、パフォーマンス的にも不利なのがよくわかりました。
Test#2: 擬似クラスを付けてul>li>a要素を選択する
次に、li要素の兄弟要素のグループ内での位置を指定してa要素を選択するケースです。liに擬似クラス:nth-child(), :first-child, および:last-childのいずれかを付けた場合と、位置を表すclass名で代用した場合とを比較しました。
<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>
class名で代用したセレクターと比較して、擬似クラスを用いたセレクターは相対的にやや速度が落ちるようです。
Test#3: 兄弟結合子を使ってdivを選択する
次は、10,000個のdiv.boxに対して、隣接兄弟結合子(+
)および一般兄弟結合子(~
)を使ったセレクターで要素を選択した場合です。div.box要素には連番でそれぞれ一意のclass名b1, b2, ...を振ってあります(下図)。
先行要素に".b5000"を持つ後続要素".box"を選択するセレクター".b5000 ~ .box"だけが非常に遅く、約596msもかかっています。
Test#4: 多数のclassを持つ要素に対してclass名で選択する
次は、各要素にclassを30個付けた状態でそのうちの1つのclassをセレクターに指定した場合です。
Before: 30個のclassを付ける前の状態でテスト
まずは比較のため、classが1〜2個しか付いていない初期状態で実行した時の結果です。
After: classを30個付けた状態でテスト
こちらは要素にclassを30個付けて、セレクターには15番目のclass名を指定して要素を選択した時の結果です。
要素のみのシンプルなセレクター(div, ul, li, a)はBefore/After共に2ms程度で増加も0.7ms以下なのに対して、class名を指定した方は3〜4ms遅くなっています。
Test#5: 長いclass名を持つ要素に対してclass名で選択する
次は、各要素に30文字程度の長いclass名を付けてそのclass名をセレクターに指定した場合です。
初期状態(Test#4 Beforeの表)よりはスピードが落ちますが、classが30個のケース(Test#4)ほどには低下しません。
Test#6: 孤立したclass名をセレクターの前に付けて選択する
Test#6〜#8は、どのHTML要素からも参照されていないclass名".notdef"をセレクターの前に付けた場合です。以下のような事例を想定しました。
- 「処理の状態」や「機能の有無」を表す文字列をJavaScriptを使ってbody要素のclassに追加しているページ
- CSSファイルを流用したため、そのHTMLページで使われていないCSSルールが大量にある。
まずはシンプルなセレクターに".notdef"を付けた場合。
Test#4 Beforeの表と比べて、セレクターのスピードに目立った低下はありません。
Test#7: 孤立したclass名をセレクターの前に付けて選択する(#6 + #4)
次に、要素が多数のclassを持っているページで(Test#4)、".notdef"をセレクターの前に付けた場合です。
シンプルなセレクターも同様にTest#6の結果と比べて4〜5ms遅くなっています。
Test#8: 孤立したclass名をセレクターの前に付けて選択する(#6 + #5)
最後は、要素が長いclass名を持っているページで(Test#5) 、".notdef"をセレクターの前に付けた場合です。
シンプルなセレクターも同様に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
-
ちなみに某ニュースサイトのページでbody要素の下のノード数をquerySelectorAll()でカウントすると3000〜3500個くらいです。 ↩