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

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](https://www.sitepoint.com/optimizing-css-id-selectors-and-other-myths/)
[資料2] CodePen: [CSS selector performance](https://codepen.io/ivancuric/pen/ZaWxqV)

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

```html: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ページであってもセレクターの書き方の違いによるパフォーマンスの差は数ミリ秒程度のものでした。
[^1]: ちなみに某ニュースサイトのページでbody要素の下のノード数をquerySelectorAll()でカウントすると3000〜3500個くらいです。

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

```html: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: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<br>ただし、特定のパターンで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要素」を表すセレクターのパターンについては、以下の５つの項目の組み合わせで構成されるセレクターとしました。他にもIDセレクターや全称セレクターが含まれるケースも考えられますが、キリがないので除外しました。

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

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

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

まずは速い方から。

**速いセレクター上位20**
![selector-1_fix.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/469745/193207d3-cd27-e7cf-a6eb-f5cf75c002a3.png)
a要素にclassを付けない場合は、祖先のul要素とのペアよりも親要素liと組み合わせたパターンにする方がパフォーマンスが若干良いようです。

**遅いセレクター下位20**
![selector-2_fix.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/469745/a2a502aa-18f6-f1d1-742e-ab9852675ca3.png)
パターンが複雑になり詳細度の下２桁が大きいセレクターでは、プラス1〜2ms処理時間が増えてきました。`ul.list`のように「要素名.class名」と言う形式は詳細度が増えて扱いづらいだけでなく、パフォーマンス的にも不利なのがよくわかりました。

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

```html: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](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/469745/03709f5b-1f12-0360-4723-2624d17ec1be.png)
class名で代用したセレクターと比較して、擬似クラスを用いたセレクターは相対的にやや速度が落ちるようです。

### Test#3: 兄弟結合子を使ってdivを選択する
次は、10,000個のdiv.boxに対して、隣接兄弟結合子（`+`）および一般兄弟結合子（`~`）を使ったセレクターで要素を選択した場合です。div.box要素には連番でそれぞれ一意のclass名b1, b2, ...を振ってあります（下図）。
![html-2_50p.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/469745/59472ed0-efbe-b47d-cf5a-182b7fd9fd89.png)
![selector-4_fix.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/469745/ee174e0c-9640-b874-622c-0cdb7f47bad2.png)
先行要素に".b5000"を持つ後続要素".box"を選択するセレクター".b5000 ~ .box"だけが非常に遅く、約596msもかかっています。

### Test#4: 多数のclassを持つ要素に対してclass名で選択する
次は、各要素にclassを30個付けた状態でそのうちの１つのclassをセレクターに指定した場合です。
![selector-31_fix.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/469745/4046f078-ad9c-7b77-4f25-bb9d06e9164a.png)
**Before:** 30個のclassを付ける前の状態でテスト
まずは比較のため、classが1〜2個しか付いていない初期状態で実行した時の結果です。
![selector-23_fix.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/469745/32276fa1-54a9-d150-61f9-90a9b41f9939.png)
**After:** classを30個付けた状態でテスト
こちらは要素にclassを30個付けて、セレクターには15番目のclass名を指定して要素を選択した時の結果です。
![selector-21_fix.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/469745/82f39037-a72d-4425-2789-de2d413e9768.png)
要素のみのシンプルなセレクター(div, ul, li, a)はBefore/After共に2ms程度で増加も0.7ms以下なのに対して、class名を指定した方は3〜4ms遅くなっています。

### Test#5: 長いclass名を持つ要素に対してclass名で選択する
次は、各要素に30文字程度の長いclass名を付けてそのclass名をセレクターに指定した場合です。
![selector-32_fix.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/469745/33304ad9-33cf-f396-c466-cc96190aad37.png)
![selector-22_fix.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/469745/63c6c5c8-6d3f-deba-cefe-6afcf297c0e3.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](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/469745/789e03e0-3197-4869-7fda-5d8d0e022ca1.png)
Test#4 Beforeの表と比べて、セレクターのスピードに目立った低下はありません。

### Test#7: 孤立したclass名をセレクターの前に付けて選択する(#6 + #4)
次に、要素が多数のclassを持っているページで(Test#4)、".notdef"をセレクターの前に付けた場合です。
![selector-21a_fix.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/469745/e88c2a9f-2fbf-f034-d9cc-0129f38f2e02.png)
シンプルなセレクターも同様にTest#6の結果と比べて4〜5ms遅くなっています。

### Test#8: 孤立したclass名をセレクターの前に付けて選択する(#6 + #5) 	
最後は、要素が長いclass名を持っているページで(Test#5) 、".notdef"をセレクターの前に付けた場合です。
![selector-22a_fix.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/469745/83440bc9-f96d-b676-41f8-995f6e6bbdac.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