🍣

スプライティング、役目を終えたテクニック

2023/12/18に公開

この記事はスターフェスティバル Advent Calendar 2023の 18 日目の記事です。
https://qiita.com/advent-calendar/2023/stafes


僕が関わっているプロダクトには「sprite.svg」という大きなsvgファイルがあります。
こいつは何だろうと思いつつ、フロントエンドに疎いこともあり特に調べることもせずにしばらく過ごしていましたが、先日 HTTP/3 について調べている中で「スプライティング」というテクニックを知り、ようやくこいつの存在理由を理解することができました。

今更ではありますが、せっかくなのでこの記事ではスプライティングというテクニックについての説明と、このテクニックが生まれた背景、そして今では必要のなくなった理由を紹介できればと思います。

スプライティングとは

http2 explained というサイトでは以下のように説明されています。

小さな画像をより集めて一つの大きな画像に結合することをスプライティングと言います。
https://http2-explained.haxx.se/ja/part3#31-supuraitingu

冒頭で触れた「sprite.svg」もまさにこのような形になっており、複数の SVG が結合された1つの大きな SVG になっていました。

そして、 HTML では以下のようにして必要な部分だけ切り取って表示します。

<svg>
    <use href="/path/to/sprite.svg#hoge"></use>
</svg>

僕のプロダクトでは SVG でしたが、 PNG などの画像ファイルでも CSS などと合わせてスプライティングは活用されます。

なぜこんなことが必要だったのか

複数の画像を1つのファイルにまとめ、表示する時はそのうちの一部分だけ切り取るという、ただ画像を表示するだけなのになぜこんなに回りくどいことをしているのかと僕は思いました。
しかし、 HTTP/1.1 の通信を行う上での制約を知るとその理由が見えてきます。

HTTP/1.1 では直列に通信する必要がある

まず、 HTTP/1.1 では 1 つの TCP コネクション上で直列に通信を行う必要があります。

1つ目の通信が終わったら2つ目の通信を始め、2つ目が終わったら3つ目...といった形で順番に通信をしていくという感じですね。

レスポンスが来てから次の通信を始めるため通信と通信の間に無駄な時間が発生していることが上の図から見てとれると思いますが、これはつまり「通信回数が多くなればなるほど無駄な時間が増える」ということになります。
そして無駄な時間が増えるということは、ページのレンダリングがそれだけ遅くなるということでもあります。

これの対策として HTTP Pipelining というコネクション管理モデルが考えられました。
これは、レスポンスを待たずにリクエストだけ先にサーバーに送りつけてしまうことで無駄な待ち時間を削減することが狙いです。

しかし実際には HTTP 層 の Head-of-Line-Blocking 問題などで思うようにパフォーマンスが上がらず有効に活用されることはなかったため、通信回数をいかに減らすかといった点は重要なままとなりました。

HTTP/1.1 ではコネクション数に上限がある

1つの TCP コネクション上では通信を直列にしか扱えませんが、コネクションを複数用意すればその分並列に通信を行うことが可能です。
であれば、たくさんコネクションを増やすことで先ほどの制約を緩和することができそうです。

しかし残念ながら、1つのホストにつき TCP コネクションを生成できる上限は 6 個ほどと決まっています。

つまり通常では最大6個の通信しか並列化されないため、1つのページを表示するのに大量の通信が必要な現代のサイトではやはり色々と厳しいわけです。

通常ではと言ったのは、ドメインシャーディングというテクニックを使ってこういった制約を突破していたサイトもあるからです。
ドメインシャーディングというのは、ホストごとにコネクションの数に上限があるのであればホストの数を増やしちゃえばいいじゃないという発想で、 a.cdn.example.com, b.cdn.example.com ... のようにドメインを大量に発行し、さまざまなホストから静的ファイルを配信するテクニックです。

後述しますが、これも HTTP/2 以降の世界では不要になったテクニックといえます。

通信効率を最大化するために必要だったこと

これまで説明した制約の上で通信効率を最大化するためには

  • 通信回数を少なくする
  • 通信の並列数を増やす

の2つのアプローチが必要になり、後者に関しては先ほど説明したドメインシャーディングというテクニックが有効でした。

そして前者の通信回数を少なくするアプローチで使用されていたのがこの記事のタイトルにもなっているスプライティングだったというわけです。

似たようなテクニックとして、スプライティングの他にはインライニングという CSS ファイルに画像の base64 を埋め込むテクニックや、 js ファイルを全て1つにまとめるコンカチネーションといったテクニックもありました。

これらのテクニックが必要なくなった理由

HTTP/1.1 までの世界では通信回数を減らすことは大きなメリットのため、スプライティングやインライニングといったテクニックはパフォーマンスの向上に役立っていました。

しかし、ファイルの一部に変更が発生するたびにファイル全体を取得し直さないといけないためキャッシュ効率が悪かったり、そのページに不要な情報もダウンロードしてしまうため必要以上に通信量が多くなってしまったりと、決して理想的なテクニックではありません。

こういった状況を大きく改善したのが HTTP/2 です。

HTTP/2 による通信の多重化

HTTP/2 では、通信の多重化を行うことで 1 つの TCP コネクション上で複数の通信を並列に処理することができるようになりました。

これによってコネクションを複数作る必要はなくなり、レスポンスも並列で受け取れるようになったため HTTP Pipelining が本来やりたかったこと以上のパフォーマンスの改善が実現し

  • 通信回数を少なくする
  • 通信の並列数を増やす

このどちらも気にする必要がなくなりました。

つまり、わざわざスプライティングなどしなくともそれぞれのリソースごとにリクエストを飛ばせばいいようになったわけです。

HTTP/3 によるパケットロスへの耐性の強化

一方で、HTTP/2 には TCP の Head-of-Line-Block 問題 が存在し、パケットロスが頻発するような不安定な通信環境だと HTTP/1.1 の方がパフォーマンスが良くなる場合があるという課題[1]もありました。

そのため、サービスを提供する地域の通信環境などによっては HTTP/1.1 の方がパフォーマンスが高いということもあったかもしれません(本当にあったのかは知りませんが...)

HTTP/3 では TCP の代わりに QUIC というトランスポートプロトコルを採用することでその問題を解決しており、スプライティングを行う理由はこれでほとんどなくなったように思います。

(ここらへんの話はフロントエンドカンファレンス沖縄 2023 でもしたので良ければ見てください!)
https://speakerdeck.com/yahiru/3-debian-warukoto?slide=8

おわりに

この記事では、スプライティング・インライニング・コンカチネーションといった通信回数を減らすためのテクニック、なぜ通信回数を減らす必要があったのかという背景、そしてHTTP/2の登場で通信回数を減らす必要がなくなった理由までを説明しました。

みなさんの現場にも sprite.svg のようなものがあれば、ぜひ一緒に分解をがんばりましょう!
ウオー!

参考

脚注
  1. パフォーマンスに関連する要素が多いため一概にはいえないが、HTTP/2がパケットロスに弱いのはそう ↩︎

スタフェステックブログ

Discussion