CSS 最適化によるパフォーマンス改善

破綻する CSS

以前、CSS が破綻する原因を 破綻しにくい CSS 設計手法と命名規則 の記事で書きました。

破綻しにくい CSS の設計手法はいくつか考案されていますが、いずれも完璧ではありません。多くのサイトでは、CSS が適切にメンテナンスされておらず、肥大化しています。肥大化した CSS はブラウザのレンダリングパフォーマンスに悪影響を与えます。

本ページでは、CSS を最適化しパフォーマンスを向上させる方法を説明します。ただし、CSS の最適化はデータを圧縮するためパフォーマンスは向上しますが、メンテナンス性は損なわれます。そのため、メンテナンスを行う CSS ファイルと、HTML が読み込む CSS ファイルは別ファイルにすると良いでしょう。

CSS ファイルサイズの削減

CSS ファイルのサイズが大きくなると、読み込みに時間がかかる原因になります。CSS ファイルはデザインに影響を与えずにファイルサイズを削減することができます。CSS ファイルのサイズを削減するには、以下の方法があります。

  • Minify 化
  • 使われていないルールセットの削除
  • 重複しているルールセットの削除
  • ショートハンドプロパティ

Minify 化

Minify 化とは、処理に必要のないコメントや空白、改行を削除することでデータ量の削減を行うことです。Minify 化するには CMS のプラグイン、オンラインサービス、プログラムのライブラリやモジュールなどがあります。以下は Minify 化を行った例です。

/* Minify 化前 */
.hoge {
    margin: 10px 0 10px 0;
    padding: 15px 15px;
    background-color: #FFFFFF;
    border: 1px solid #E2E2E2;
    /* border-radius: 4px; */
}

/* Minify 化後 */
.hoge{margin:10px 0;padding:15px;background-color:#FFF;border:1px solid #E2E2E2}
Minify 化を行った例

使われていないルールセットの削除

CSS が適切にメンテナンスされていないと、使われていない古いルールセットが残り続けている場合があります。不要なルールセットは、ブラウザに不要な計算をさせるため、パフォーマンスにも悪影響を与えます。そのため、どのページでも使われなくなった古いルールセットは削除します。

使われていないルールセットを手動で調べるのは大変なので、UnCSS などのモジュールを利用すると良いでしょう。

ただし、どのページでも使われていないルールセットであっても削除できないパターンがあります。将来的に使う場合、Javascript などのプログラムで使う場合、リスト化している場合などがあります。

.width10px { width: 10px; }
.width20px { width: 20px; }
.width30px { width: 30px; }
.width40px { width: 40px; }
.width50px { width: 50px; }
ルールセットがリスト化されている例

上記の場合では、汎用性を持たせるためにリスト化しているので、削除してしまうとバグの原因になります。

重複しているルールセットの削除

重複しているルールセットを削除する作業は、使われていないルールセットの削除よりも難しい作業で、リスクを伴います。最終的に適用されるルールセットがわからないと、必要なルールセットまで削除する可能性があります。以下は、セレクタ内での重複しているパターンと、別のクラスで上書きしているパターンです。

/* セレクタ内での重複 */
.hoge {
    width: 100px;
    ... (中略) ...
    width: 200px;
}

/* .foo を .bar で上書き */
.foo { width: 10px; }
.bar { width: 20px; }
ルールセットが重複している例

上記は簡略化した例ですが、実際はもっと複雑な状態になっています。複雑化した CSS をクリーンな状態に戻すのは、絡まりあった糸を解くようなもので、簡単な作業ではありません。

安易な解決策として !important を使った優先度の変更があります。しかし、この方法は CSS をより複雑化させるためにアンチパターン、または最後の手段として知られています。

最終的に適用されるスタイルは "詳細度" を算出することによって決まります。詳細度は、a, b, c の 3 つの数値の大きさによって決まります。以下はセレクタを詳細度の小さい順に並べています。

  1. *a=0, b=0, c=0
  2. lia=0, b=0, c=1
  3. ul lia=0, b=0, c=2
  4. ul ol+lia=0, b=0, c=3
  5. h1 + *[rel=up]a=0, b=1, c=1
  6. ul ol li.reda=0, b=1, c=3
  7. li.red.levela=0, b=2, c=1
  8. #foobara=1, b=0, c=0
  9. #foobar:not(li)a=1, b=0, c=1

a, b, c は桁数のようなもので、a > b > c の関係になります。詳細度が同じ場合は、後から宣言されたルールセットが優先されます。

重複しているルールセットの削除は、詳細度を考慮して行います。ただし、手動で行うのは大変なので CSSCSS などのモジュールを利用すると効率的です。

ショートハンドプロパティ

CSS には複数のプロパティをまとめて指定できる "ショートハンドプロパティ" という方法があります。ショートハンドで表現できるプロパティの種類が多いため本ページでは割愛しますが、ショートハンドを利用することでファイルサイズを削減できます。以下はショートハンドプロパティで表現した例になります。

/* 最適化前 */
background-color: #000;
background-image: url(images/bg.gif);
background-repeat: no-repeat;
background-position: left top;

/* 最適化後 */
background: #000 url(images/bg.gif) no-repeat left top;
ショートハンドプロパティの例

ショートハンドプロパティは複数のプロパティを指定できますが、省略したプロパティには初期値が設定されます。そのため、設定済みのプロパティを初期値で上書きするため、使用には注意が必要です。

クリーンな CSS に変換するモジュール

CSS ファイルサイズを最小にしたい場合、変換するエンジンがあります。例えば、以下は "cssnano" というモジュールによって CSS ファイルを圧縮した例になります。

/* 最適化前 */
/* normalize selectors */
h1::before, h1:before {
    /* reduce shorthand even further */
    margin: 10px 20px 10px 20px;
    /* reduce color values */
    color: #ff0000;
    /* remove duplicated properties */
    font-weight: 400;
    font-weight: 400;
    /* reduce position values */
    background-position: bottom right;
    /* replace initial values */
    min-width: initial;
}
/* correct invalid placement */
@charset "utf-8";

/* 最適化後 */
@charset "utf-8";h1:before{margin:10px 20px;color:red;font-weight:400;background-position:100% 100%;min-width:0}
cssnano によって最小化した例

上記の例では、ショートハンドプロパティへの置き換え、重複したプロパティの削除、文字列の変換など、様々な最適化が行われています。このように CSS をクリーンな状態にするモジュールはいくつもあります。以下のリンクは、その一例になります。

CSS セレクタの最適化

CSS のセレクタはパフォーマンスに影響を与えます。CSS のセレクタは、HTML ドキュメントの要素とマッチング処理を何回も行うため、効率的にマッチングできるセレクタの方がパフォーマンス的に有利になるからです。CSS のセレクタは、一般的に以下の順番でパフォーマンスが良いとされています。

  1. ID (#hoge)
  2. Class (.hoge)
  3. Tag (div)
  4. Adjacent sibling (h2 + p)
  5. Child (li > ul)
  6. Descendant (ul li)
  7. Universal (*)
  8. Attribute ([type="text"])
  9. Pseudo-classes (a:hover)

セレクタは組み合わせた複合的な指定が可能ですが、複雑な指定ほどマッチング処理に時間がかかり、パフォーマンスに悪影響を与えます。CSS セレクタの最適化を行うには ID や Class を使ってシンプルに定義します。

スタイルの計算では、各要素をすべてのスタイルとマッチングするかのチェックを行います。そのため、計算コストの最悪なケースは要素数とセレクタ数の積に比例します。複合的なセレクタでは計算コストが悪化するだけでなく、メンテナンス性も悪くなるため避けた方が無難です。

しかし、CSS セレクタを最適化することで得られるメリットは大きくありません。少しデータは古いですが、ハイパフォーマンス Web サイトの著者である Steve Souders は以下のページで検証結果を記載しており、最適化を行っても平均で 50 ms 程度しか改善していない結果が出ています。

モダンブラウザでは処理効率が改善されており、CSS セレクタを複雑に指定してもパフォーマンスに差が出にくくなっています。セレクタによるパフォーマンスチューニングは、メンテナンス性を考慮しつつ行いましょう。

レンダリングの最適化

ブラウザがページを表示するまでにはいくつもの処理が行われています。ここでは HTML ファイルと CSS ファイルがダウンロードされ、レンダリングされるまでの最適化について説明します。

ダウンロードされた HTML ファイルは "DOM (Document Object Model)" に変換され、CSS ファイルは "CSSOM (CSS Object Model)" に変換されます。DOM と CSSOM には処理コストがあるため、レンダリングを最適化することで処理コストを改善することができます。DOM と CSSOM の構築コストと処理コストは DevTools で計測することができます。

DevTools による構築コストと処理コストの計測
DevTools による構築コストと処理コストの計測

ブラウザは DOM と CSSOM から "Rendering Tree" を構築します。レンダリングツリーが正しく構築されると "Layout""Paint" の工程に進みます。このような一連の流れは "クリティカルレンダリングパス" と呼びます。

ブラウザのレンダリングフロー
ブラウザのレンダリングフロー
  1. Loading:リソースをダウンロードする
  2. Scripting:Javascript を実行する
  3. Rendering:レンダリングツリーを構築する
  4. Painting:描画する

クリティカルレンダリングパスは、"コンテンツ表示の順位付け" を意味します。クリティカルレンダリングパスを考える場合、ユーザにとって何が重要なコンテンツであるかを考慮して最適化を行います。

ページが表示されるまで白い画面になっているのは、描画されるまでのいずれかの工程に時間がかかっているからです。高速化するためには、描画までの時間を小さくする必要があります。どこがボトルネックになっているか確認するためには DevTools から確認できます。

DevTools によるパフォーマンス計測
DevTools によるパフォーマンス計測

CSS はレンダリングをブロックするリソースとして扱われます。そのため、CSS が読み込まれて CSSOM の構築が完了するまでレンダリングが保留されます。

また、CSSOM はキャッシュという仕組みを持ちません。CSS ファイルをブラウザがキャッシュしても、毎回 CSSOM の再構築が行われます。そのため、CSSOM を最適化するためには、レンダリングをブロックするリソースと、ブロックしないリソースに分割する方法があります。

例えば、メディアクエリやメディアタイプを利用したリソースはレンダリングをブロックしません。印刷時にしか使わない CSS などは別ファイルに分割し、レンダリングをブロックするリソースの後続に配置すると、レンダリングの邪魔になりません。以下は、レンダリングブロックを考慮した CSS 定義の例になります。

<!-- 最適化前 -->
<link href="style.css" rel="stylesheet">

<!-- 最適化後 -->
<link href="style.css" rel="stylesheet">
<link href="print.css" rel="stylesheet" media="print">
レンダリングブロックリソースを先に読み込ませる例

Google のパフォーマンス計測サイトである PageSpeed Insights では、ページが表示されてスクロールせずに見える範囲 (ファーストビュー) のレンダリングがブロックされていると警告が表示されます。この警告も同様に CSS がレンダリングをブロックしているために体感速度を下げていることが原因です。詳細は、PageSpeed Insights によるパフォーマンス測定と改善方法 を参照してください。

本章では、Rendering 工程で行われる "Calculate Style""Layout" について説明します。

Calculate Style

Calcuate Style では、ドキュメントから構成された DOM ツリーの要素に対して、適用される CSS プロパティを計算します。計算は CSSOM ツリーを参照し、マッチングするセレクタが存在するか総当たりで行われます。

DOM ツリーのすべての要素に対して、総当たりでマッチング処理を行うため、計算効率は良いとは言えません。例えば、要素が 100 個のドキュメントに対して、ルールセットが 50 個あった場合、5,000 回のマッチング処理が行われます。

マッチング処理は高速に行われますが、回数が多くなるとボトルネックになります。マッチング処理を高速化するためには、以下の対処を行いマッチング回数を減らすことが有効です。

  • DOM を必要以上に深くしないこと
  • CSS ルールセットを最小限にすること
  • 子孫、間接、全称セレクタを多用しないこと

Layout

Calcuate Style が完了すると、Layout の計算を行います。Layout はレンダリングエンジンによって "Reflow" とも呼ばれます。WebKit 系のレンダリングエンジンでは Layout と呼び、Gecko 系のレンダリングエンジンでは Reflow と呼びます。

レイアウトとは、ブラウザがレンダリングするために要素の位置関係などを再計算するための処理のことです。レイアウトが発生する原因は、プロパティ値を相対値で指定、Javascript の使用、DOM ツリーの変更、ウィンドウサイズの変更などがあります。

レイアウトが発生した場合、その分だけ描画するまでの時間が長くなります。そのため、レイアウトは可能な限り発生させないことがパフォーマンスの改善につながります。以下は、Firefox でリフローが行われる様子を可視化したデモ動画です。

レイアウトを最小限に抑えるための方法は、前述した Calculate Style と同様になります。

クロスブラウザ対応

CSS では新しいプロパティが次々に登場しています。Chrome や Firefox などのモダンブラウザではその恩恵を受けやすく、従来に比べてデザインを柔軟に構築できます。しかし、一方でレガシーブラウザを利用しているユーザもいるため、クロスブラウザ対応の問題はデザイナーの悩みのタネです。

ブラウザの種類やバージョンは多岐にわたるため、すべて対応することは不可能です。一部のサイトではレガシーブラウザの動作確認を行わずにモダンブラウザのみを対象としています。どこまで対応するかは、サイトの特性や動作確認のコスト次第になります。

本サイトでも、基本的にはモダンブラウザのみ動作確認を行っています。どこまでのブラウザの種類に対応するかは、Google Analytics からユーザ環境の割合を見て判断しています。

新しいプロパティはレガシーブラウザは対応していないため、なるべく使い古されたプロパティを優先的に選択しています。

まとめ

CSS はメンテナンス、パフォーマンスチューニング、クロスブラウザ対応など、管理するだけでも大変な労力が必要になります。おまけに、配信するサーバの性能やネットワーク帯域、レンダリングを行うブラウザ、ユーザの端末スペックなどを考慮すると CSS の最適化はそこまで考慮するべき作業ではないかもしれません。そして、仮に CSS を最適化しても、目に見えるほどの大きなリターンが得られるわけでもありません。

しかし、本サイトでは労力に見合わない作業であっても、ユーザビリティに少しでも寄与できるのであれば取り入れていきたいと思います。本記事がサイトを運営している方へ CSS 最適化の動機付けになればと思います。

Category:
パフォーマンス
公開日:
更新日:
Pageviews:
4,542
Shares:
24
Tag:
CSS
PageSpeed Insights
コンテンツ制作