CSSスタイル計算とレイアウトの効率化

# はじめに
ウェブページをブラウザで表示する際、背後では複雑なレンダリングプロセスが実行されています。
今回は、レンダリングプロセスの中の工程である **Rendering（スタイル計算（ Calculate Style ）とレイアウト（ Lauout ））** のパフォーマンスを向上するためのテクニックについて、記事を書いていこうと思います。

# 関連記事
本記事に関連した記事も投稿したので、ぜひ読んでいただけると幸いです！

- [ブラウザレンダリングのプロセスを理解する](https://qiita.com/fujisho1216/items/94a4f74379642f6f282f)
- [ブラウザのリソース読み込みを最適化するためのパフォーマンス向上テクニック](https://qiita.com/fujisho1216/items/d53443c6eb5e2a8acc34)
- [レンダリングプロセスにおける Scripting の高速化テクニック](https://qiita.com/fujisho1216/items/5a3e16cfd3c22c77f8ea)

# 参考書
[Webフロントエンド ハイパフォーマンス チューニング](https://www.amazon.co.jp/Web%E3%83%95%E3%83%AD%E3%83%B3%E3%83%88%E3%82%A8%E3%83%B3%E3%83%89-%E3%83%8F%E3%82%A4%E3%83%91%E3%83%95%E3%82%A9%E3%83%BC%E3%83%9E%E3%83%B3%E3%82%B9-%E3%83%81%E3%83%A5%E3%83%BC%E3%83%8B%E3%83%B3%E3%82%B0-%E4%B9%85%E4%BF%9D%E7%94%B0-%E5%85%89%E5%89%87/dp/4774189677)

# 前提
Rendering フェーズでは、各 DOM 要素のスタイル計算（ Calculate Style ）とレイアウト（ Lauout ）　の 2 つの処理が行われます。

## Calculate Style
ここでは、 CSSOM ツリーに格納されている CSS ルールセットがどの DOM 要素に対して適用されるのかを計算します。
具体的には、 CSS ルールセットのセレクタのマッチング処理をドキュメントの DOM ツリーに格納される DOM 要素に対して行います。

その後、 CSS ルールセットの詳細度にしたがって DOM 要素に当たる CSS プロパティが計算されます。

## Layout
Calculate Styleが終わった後に実行されるのが Layout 処理です。
ここでは、 DOM 要素を含む視覚的な要素ごとの Layout が行われます。

つまり、適用される CSS プロパティを元にして具体的な座標や大きさなどを計算します。

# レイアウトツリー構築( Rendering ）の最適化のテクニック
JavaScript で DOM ツリーの操作を行う場合、多くの場合 Layout が引き起こされます。
そのため、レイアウトツリー構築（ Rendering ）を高速化することは、アプリケーション全体のパフォーマンス改善に大きく寄与します。

今回は、そんなレイアウトツリー構築( Rendering ）の最適化のテクニックについて記事を書いていきたいと思います。

# Calculate Style の高速化

## 1. CSS セレクタをシンプルにする
そもそも CSS セレクタのマッチング処理は、右から左へと行われます。
そのため、セレクタが複雑であればあるほど、マッチング処理に時間がかかります。
例えば、以下のように階層を深く指定したセレクタは、すべての試行を行う必要があり、パフォーマンスに悪影響を与えます。

```css
/* 非効率なセレクタ */
table > tbody > tr > td {
  /* スタイル */
}

/* より効率的なセレクタ */
td {
  /* スタイル */
}
```

シンプルなセレクタにすることで、不要な試行を減らし、レンダリングパフォーマンスを向上させることができます。

## 2. 子孫セレクタ・間接セレクタを避ける

子孫セレクタや間接セレクタは、複数の DOM 要素にマッチするため、処理が遅くなります。
特に、ID やクラスに対して直接指定するセレクタのほうが効率的です。

```css
/* 非効率な子孫セレクタ */
header p {
  /* スタイル */
}

/* より効率的なセレクタ */
.header p {
  /* スタイル */
}
```

可能な限り、特定の要素に直接適用されるクラスや ID を活用し、間接的なセレクタを避けることで、マッチング処理が高速化されて、パフォーマンス向上につながります。

## 3. 全称セレクタの使用を避ける

全称セレクタ（`*`）は、すべての DOM 要素に適用されるため、特にパフォーマンスに悪影響を与えます。

CSS ルールセットの適用範囲が広がりすぎることを避けるため、特定の要素やクラスを明示的に指定する方が良いです。

```css
/* 全称セレクタ */
* {
  /* スタイル */
}

/* （良い例）特定のクラスに限定 */
.header * {
  /* スタイル */
}

```

---

### 余談
そもそも、全称セレクタ / 子孫セレクタ / 間接セレクタは、極力使用しないほうが良い理由は、コード可読性やモダンな CSS 設計手法（ BEM...etc ）と親和性といったメリットがあると感じています。

以下に具体的なメリットを記述します。

#### 1. コードの可読性が向上する
全称セレクタ / 子孫セレクタ / 間接セレクタを使用すると、スタイルがどの要素に適用されるかを理解するために、複数のDOM要素を追跡する必要があり、コードの理解に時間がかかります。

クラスやIDを使うことで「どの要素にスタイルが適用されているか」が視覚的にわかりやすくなります。

#### 2. **保守性が向上する**

全称セレクタ / 子孫セレクタ / 間接セレクタを使用すると、影響範囲が広くなりすぎて、保守性が低い状態になります。（意図しないスタイルが付与されたりする）

ID やクラスを用いて影響範囲を狭める + 予測しやすい状態にすることで、保守性の高い状態にすることができます。

#### 3. BEM や OOCSS などの設計手法との親和性

BEM（ Block Element Modifier ）や OOCSS（ Object-Oriented CSS ）といったモダンな CSS 設計手法は、クラスベースのセレクタを推奨しています。
これにより、セレクタの構造が一貫し、再利用可能な CSS を作成することができるようになります。

---

## 4. モダンな CSS 設計手法( BEM..etc )との親和性
（今回は、 BEM（ Block Element Modifier ）を例に挙げて解説します。）

BEM は、CSS セレクタのマッチング処理において高いパフォーマンスを提供します。
これが可能となる主な理由は、BEM の命名規則が、基本的に 1 つのクラスセレクタの使用を強制するからです。

通常、CSS ルールセットではセレクタの詳細度に応じて優先度が決定されますが、BEM ではセレクタをクラスセレクタのみに制限することで、この詳細度が一貫化されます。
これにより、CSS セレクタのマッチング処理のオーバーヘッドが最小限に抑えられ、全体のパフォーマンスを向上することができます。

## 5. CSS セレクタのマッチング処理を避ける
Recalculate Style を高速化するには、そもそも CSS セレクタのマッチング処理をレンダリングエンジンに行わせないほうが有効な場合もあります。
その手法を紹介します。

### 1. DOM 要素のクラスやスタイルを変更する

CSS セレクタのマッチング処理を避け、パフォーマンスを向上させるためには、DOM 要素のクラス属性やスタイルプロパティを JavaScript で動的に変更することが有効です。
JavaScript で動的にクラスやスタイルを変更することで、Recalculate Style のコストを削減し、レンダリングのパフォーマンスを向上させることができます。

特に、クラス属性を操作することで、事前に定義された CSS ルールセットを適用でき、効率的なスタイル適用が可能となります。

### 2. 利用していない CSS ルールセットを削減する

利用されていない CSS ルールセットを削減することで、CSS セレクタのマッチング処理に利用されるルールの数を減らし、パフォーマンスを向上させることができます。

特に、大規模なプロジェクトでは、未使用の CSS を検出し、削減するために `UNCSS` のようなツールを利用することが推奨されます。

### 3. メディアクエリを指定する
切にメディアクエリを使用することで、特定の条件下でのみ適用されるCSSルールセットを限定し、不要なCSSのマッチング処理を減らすことができます。これにより、レンダリング時のパフォーマンスが向上します。

#### 前提
CSSのメディアクエリは、特定の条件（例えば、画面の幅、高さ、解像度、向きなど）に基づいてスタイルを適用するツールです。

#### 例
下では、画面幅が `768px` 以下の場合にのみ `.container` クラスにスタイルが適用されるようになり、条件に合わない場合はこのスタイルが無視されます。
これにより、マッチング処理の回数が減り、パフォーマンスが向上します。

```css
@media (max-width: 768px) {
  .container {
    display: block;
    padding: 10px;
  }
}
```

# Layout の高速化
Calculate Style が終了すると、今度は DOM ツリーから視覚的な要素のみを抜き出して、それらの Layout を行います。

Layout を引き起こす典型的な原因は、下記の 3 つです。

- DOM 要素の座標や大きさの変化
- DOM ツリーの構造の変化
- DOM 要素のコンテンツの変化

## 典型的な原因の詳細
上記の原因がどのような状況で起こりうるのか解説します。

### 1. DOM 要素の座標や大きさの変化

DOM 要素の位置や大きさに関連する CSS プロパティの変更は、 レイアウトに直接影響を与えます。
例えば、要素の `width` や `height`、`padding`、`border` のプロパティが変更されると、要素のボックスモデルが再計算され、レイアウトが更新されます。
これにより、ブラウザは新しいレイアウトを再度計算する必要があります。

### 2. DOM ツリーの構造の変化

DOM ツリーの構造が変更されると、ブラウザはレイアウトを再計算します。
例えば、新しい要素を DOM に追加したり、既存の要素を削除したりすると、これに対応してレイアウトが変更されます。
これに伴い、 `Layout`の再計算（`Calculate Style`でも再計算が実行される場合もある）が行われます。

### 3. DOM 要素のコンテンツの変化

DOM 要素内のコンテンツが変更されると、それがレイアウトに影響を与えることがあります。
特に、要素の幅や高さがコンテンツに依存している場合、コンテンツの変更がレイアウトの変更を引き起こします。
例えば、テキストの内容が変わると、その要素のサイズが再計算され、レイアウトの再配置が実行されます。

## 1. レイアウトを減らす非表示の方法
レイアウトのコストを削減するためには、対象となる要素を非表示にする（要素自体を減らすことで、Layout の対象外にする）ことが効果的です。
DOM 要素を非表示にする方法として、主に以下の2つがあります。

- visibility プロパティを `hidden` に設定する
    - 要素を視覚的に非表示にしますが、要素が占めている領域は残ります
    - そのため、レイアウトの再計算は行われませんが、要素自体はレイアウトツリーに残ります
- display プロパティを `none` に設定する（推奨）
    - 要素を完全にレイアウトツリーから除外します
    - 要素が占めていた領域も消えるため、レイアウトの再計算が行われます

補足として、`position` プロパティを `absolute` または `fixed` に設定して要素を画面外に配置した場合でも、その要素はレイアウトツリーに残り、レイアウトの計算が行われます。
そのため、完全にレイアウトを削減したい場合は、`display: none` を使用することが推奨されます。

# まとめ
今回は、レンダリングプロセスの中の工程である **Rendering（スタイル計算（ Calculate Style ）とレイアウト（ Lauout ））** のパフォーマンスを向上するためのテクニックについて書きました。
何気なく則っていた BEM という CSS 設計手法もパフォーマンス観点で見ると改めてその優秀さを実感しました。
今後とも、パフォーマンスに関する知見を深めていきたいです。