PerfData

CSS

CSSファイルの読込の高速化

2023年5月8日
著者: 竹洞 陽一郎

はじめに

今回から、複数回にわたり、CSSに関する解説を連載していきます。

前回の記事では、Preload Scannerについて説明しました。
Preload Scannerは、Shallow Parsingを用いて、読み込むべきファイルを検出し、バックグラウンドでファイルの読み込みを行います。
このプロセスには、CSSも含まれています。

しかし、Preload Scannerの恩恵を阻む機能を使うと読込が遅延してしまうのです。

CSSの読込とCSS Object Model

CSS Background Imageによる遅延

CSS Background Imageとは、CSS(Cascading Style Sheets)の機能の一つで、HTML要素の背景に画像を設定する際に使用されるスタイルプロパティです。
このプロパティを用いることで、HTML要素に対して画像を背景として表示し、デザインを豊かにすることができます。
CSS Background Imageは、以下のような形式で記述されます。


selector {
  background-image: url("path/to/image.jpg");
}

ここで、selectorは対象となるHTML要素を指定し、url("path/to/image.jpg")には表示したい画像ファイルへのパスが入ります。

CSS Background Imageの問題点

Webパフォーマンスにおいて、CSS Background Imageは、2008年に発売されたスティーブ・サウダーズ氏が書いた「ハイパフォーマンスWebサイト ―高速サイトを実現する14のルール」(オライリージャパン)で一躍注目を集めました。
CSSスプライトを利用することで、画像を統合して配信して、画像を一気に送るという手法です。

しかし、皆さん、今年は2023年です。
15年もの間、技術は停滞していると思いますか?
画像については、decoding="async"loading="lazy"属性によって、バックグラウンド処理や遅延読み込みができるようになりました。

decoding="async"を提唱したAppleのSimon Fraser氏は、2016年10月18日にGitHub上で以下のようにCSSの画像についても提案しました。

大きな画像のデコードは、メインスレッドを数百ミリ秒以上ブロックし、流れるようなアニメーションやユーザーとのインタラクションを中断することがあります。
現在、Web制作者が非同期で画像をデコードすることを指定する方法はありません。
そのため、UIの停止を避けることができないシナリオが存在します。

この問題を解決するために、画像要素に「async」属性を提案します。
この属性は、制作者が非同期デコードをリクエストしたことをUA(ユーザーエージェント)にヒントとして与えます。
これにより、「load」イベントが発火した後、画像がデコードされる前にUAが画像を描画する場合、UAは画像の描画をブロックせず、描画しないことが許可されます。

デコードされた画像フレームが利用可能になったときに制作者に通知するため、画像要素で新しいイベント「ready」を発火することを提案します。
これにより、UIの停止に敏感なコンテンツで完全にデコードされた画像が必要な制作者は、「ready」イベントを待ってから画像を表示する何かを行うことができます。
(例えば、CSSトランジション)

アニメーションGIFのように、一部の画像は繰り返しフレームをデコードします。
また、UAは静止画像のデコードされたフレームを破棄し、再デコードが必要になることがあります。
この場合、「ready」イベントは最初に表示可能なフレームが利用可能になったときに一度だけ発火することを提案します。

問題点:
「async」は現在、非同期の読み込みを意味するために使用されており、画像に対しても同じ意味で使いたい場合があります。
新しい属性の名前を「asyncDecode」などに変更することを検討してください。

これは、画像要素の問題のみを解決し、CSS画像には対応していません。
CSS画像に対して非同期デコードを許可するCSSプロパティを追加し、適用される要素にイベントを発火することができます。

このように提唱されたものの、現状では、CSS Backgroud Imageは、<img>タグで読み込む画像と異なり、decoding="async"loading="lazy"といった画像読込をメインスレッドに影響を与えなくするためのオプションが存在しません。
そのため、CSS Background Imageを使うと、CSSの処理中に画像のダウンロードを行うことになり、CSS Object Model適用を遅延させ、Webパフォーマンスのボトルネックになります。

現在の状況では、CSS Background Imageの使用は避けて、画像の読み込みにはできるだけ<img>タグを用いることが望ましいです。

CSSの@importによる遅延

CSSの@importルールは、Preload Scanner(事前読み込みスキャナ)を活用できません。
Preload ScannerはHTML内の要素を解析し、リソースの読み込みを事前に開始することが目的です。

Preload ScannerはHTMLファイル内の<link>要素や<script>要素などをスキャンしてリソースの読み込みを早めることができますが、@importルールはCSSファイル内に記述されるため、直接スキャンできません。
その結果、リソースの読み込みが遅れることがあります。

また、@importルールを使用すると、CSSファイルが1つずつ読み込まれることになり、次のCSSファイルの読み込みが開始されるまで待たなければならないため、ページのレンダリング速度が低下します。

この問題を回避するためには、@importルールの代わりに、HTMLファイル内に複数の<link rel="stylesheet">要素を配置し、リソースを並列に読み込むようにすることが推奨されます。
これにより、Preload Scannerの恩恵を受けてページのレンダリング速度が向上します。

@importによるCSSの直列読込と、<link rel="stylesheet">によるPreload Scannerによる並列読込の違い

CSSファイルの統合が速度に与える影響について

図を参照して、「複数のCSSファイルを1つに統合すれば、並列読み込みがさらに高速化されるのでは?」と疑問に思う方もいるでしょう。
例えば、以下の4つのCSSファイルを統合する場合を考えてみましょう。

この場合、統合によってファイル容量が変わるでしょうか?
答えは「いいえ」です。
統合されたファイルの容量は、4つのファイルの合計値、つまり 50KB + 40KB + 30KB + 20KB = 140KBとなります。

ブラウザやプロトコルによって違いますが、Chromeの場合、接続数は以下の通りです。

※HTTP/3はUDPベースのため、1つのQUIC接続で複数のストリームを多重化し、各ストリームが独立してデータの送受信を行うことが可能です。
ストリーム単位でファイルの送受信が行われます。
ただし、HTTP/1.1と比較して、国内の実測値では大きな差はないです。

それでは、これらのファイルはどのように割り当てられてダウンロードされるでしょうか。

HTTPプロトコルの各バージョンにおけるサーバ単位の瞬間最大転送容量とファイルの割り当て方
プロトコルLayer4サーバ単位の瞬間最大転送容量ファイルの割り当て方
HTTP/1.1 TCP MTU 1500×6コネクション=9,000byte リソースの種類や優先順位に応じて、6つのコネクションに適切に割り当てられます。
HTTP/2 TCP MTU 1500×1コネクション=1,500byte
HTTP/2では、ストリームという概念が導入されており、1つのコネクション内で複数のストリームを同時に扱うことができます。
しかし、TCP上で動作しているため、MTU1500×1車線の制限があります。
コネクション単位で見ると、HTTP/1.1とは異なり、複数のファイルのパケットが混在して送られてきます。
基本的に1つのファイルの送受信に1つのストリームを割り当てます。
ただし、ストリーム数には制限があるため、同時に複数のファイルを送受信する場合は、ストリームを使い回して効率的に送受信が行われます。
そのため、1ファイル1ストリームとは限らず、必要に応じてストリームが割り当てられます。
HTTP/3 UDP TCPのようなMTUの制限はありません。
TCPベースのHTTP/1.1やHTTP/2より瞬間最大転送容量の制限は緩やかです。
UDPはデータグラム単位でデータを送信するため、データグラムのサイズはプログラマが任意に指定できます。
しかし、UDPの場合は、データグラムが一定のサイズを超えるとIPフラグメンテーションが発生するため、実質的な制限は存在します。
UDPにはエラー検出や再送制御の仕組みがないため、QUICがこれらの処理を行います。
基本的に1つのファイルの送受信に1つのストリームを割り当てます。

実際、検証してみれば、大して違いが無い事が分かります。
是非、試してみて下さい。

CSSファイルのダウンロードを挽肉づくりに例える

ファイル転送は、挽肉づくりに似ています。
大きな肉の塊でも、小さい肉の塊でも、ミートグラインダーに入れれば、均等に挽かれて出てきます。

挽肉は、肉の粒をくっつけても元の肉の塊には戻らないですが、ファイル転送の場合は、パケット単位で送られたデータが結合されて元のファイルと同じになります。

ひき肉づくり

挽肉をつくる機械「ミートグラインダー」(肉挽き機)に1.4kgの肉の塊を入れても、500g、400g、300g、200gの合計1.4kgの肉の塊を入れても、結果として1.4kgの挽肉が出来上がります。
同様に、結合して140KBのファイルを送っても、分割された50KB + 40KB + 30KB + 20KB =合計140KBのファイルを送っても、結果として、同じような数のパケットに分割されて送られます。
もちろん、6台のミートグラインダーを使う(HTTP/1.1)、1台のミートグラインダーを使う(HTTP/2)、使えるだけのミートグラインダーを使う(HTTP/3)という違いはあります。

複数のコネクションやストリームを使うHTTP/1.1やHTTP/3であれば、むしろ分割する方が効率が上がるでしょう。
しかし、それも些細な差です。
その差は、他の処理待ち時間で吸収されてしまいます。
ですから、CSSを複数枚にするか、1枚に統合するかを気にする必要はありません。

Webパフォーマンスチューニングで不可欠なTOC(制約条件の理論)を学ぼう」で、制約条件について解説しています。
CSSの読込時間は、適切に対処していれば、制約にならない、ということです。

上述したような、バックグラウンド画像や、@importによる遅延の方が遥かに影響が大きいです。
そちらの対処に注力する方が、遥かに高速化に寄与します。

まとめ

CSSファイルの統合がページの読み込み速度に与える影響について解説しました。
結果として、複数のCSSファイルを1つに統合することがWebページの表示速度向上につながらないことが分かりました。
また、HTTPプロトコルのバージョンごとの瞬間最大転送容量とファイルの割り当て方について説明しました。

さらに、CSS Background Imageを使用している場合は、Preload Scannerを利用してバックグラウンド処理で高速にダウンロードできるように、<img>に書き換えることを推奨します。
CSSスプライトはもはや使用すべきではなく、@importを使用すると、Preload Scannerの利点が得られず、ダウンロード時間が遅延する原因となります。

最後に、CSSの読込の高速化については、Background Imageや@importによる遅延の対処に注力することで高速化に遥かに寄与できます。