短い答え
画像をデータURIにを開いて画像を入れ、CSSカスタムプロパティを選び、その行をスタイルシートの先頭に貼ります。あとは必要なところでbackground-image: var(--logo)として使ってください。
そうするのは、画像が小さく、しかもどのページでも必要な場合です。アイコン、箇条書きの点、山形記号、模様といったものです。写真ではやらないでください。以下は、この2文がなぜ違うのか、そして自分がどちらを見ているのかを見分ける方法です。
データURIとは実際何なのか
あるものを指すのではなく、そのものを含んでいるアドレスです。スタイルシートは、ふつうこう書きます。
background-image: url("logo.png"); そしてブラウザはlogo.pngを取りに行きます。データURIは、こう書きます。
background-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUg..."); そして、取りに行くものはありません。画像はすでにそこに、文字として書き出されています。部分は3つです。data:がスキーム。image/pngがメディアタイプで、ブラウザはこれを完全に信じます。それについては後述します。カンマから後ろがファイルです。
考え方は、これで全部です。小手先の技でも裏技でもありません。1998年から標準にあり、それ以降に出たすべてのブラウザで動きます。
得られるもの。往復が1回減る
節約されるのは帯域ではありません。リクエストです。
ブラウザは、logo.pngに言及しているスタイルシートを読むまでは、それを要求できません。そしてそのスタイルシートを取ってくるまでは、読めません。ですからふつうの背景画像は、ページ読み込みの中で少なくとも往復2回分の深さにあります。そして遅い回線のスマホでは、ファイルがどれだけ小さかろうと、1回の往復が数百ミリ秒になりえます。600バイトの山形記号は、転送にほとんど費用がかからないのに、届くまでに4分の1秒かかることがあるのです。
埋め込めば、スタイルシートと一緒に届きます。利点はそれで全部ですし、画面の上部に現れる小さなアイコンにとっては、それは本物の利点です。
代償。3分の1、そしてキャッシュ
base64は3分の1ほどを足します。ファイルの3バイトが4文字になります。URLが許す文字だけで任意のバイト列を書くには、それだけかかるからです。それを避ける賢いエンコーダーは存在しません。9KBのPNGは、12KBのスタイルシートです。
圧縮は、それを返してくれません。ここが、人が勝手に大丈夫だと思っている部分です。GzipやBrotliは冗長性を見つけることで働きますが、PNGもJPEGもWebPも、すでに圧縮されています。残っている冗長性はごくわずかですし、base64は何も足しません。実際に取り戻せるのは、その3分の1のさらに10分の1程度で、全部ではありません。(SVGは逆の場合で、次の節がそれについてです。)
ファイルであることをやめます。これは、たいていの測定には現れない代償であり、大きさが効いてくると問題になるほうです。
- 単独ではキャッシュされません。ふつうの画像は一度取得され、1年間再利用されます。埋め込まれた画像はスタイルシートの一部なので、スタイルシートのキャッシュ項目と運命をともにします。
- 何かを変えると、すべてが再ダウンロードされます。余白をひとつ直して新しいスタイルシートを配れば、訪問者は全員、埋め込まれた画像も一緒にもう一度ダウンロードします。2年間変わっていない画像をです。
- クリティカルパスの上に乗ります。スタイルシートは描画を止めます。画像は止めません。画像を埋め込むことは、それを後者から前者へ移すことです。画像を含む全体が届くまで、ページは描画できません。
- 並行して取得されません。ブラウザは多くのものを同時にダウンロードします。埋め込まれた画像は独立したものではないので、その恩恵をまったく受けません。
おおよその目安です。ブラウザの挙動が変わる境目ではなく、助言が変わる境目だと思ってください。2KB程度までなら明らかに得。10KB程度までなら、どのページにも出るものについてはたいてい見合います。50KBを超えると、エラーメッセージの出ない間違いです。この道具は、各結果がどの帯に落ちるのかを、文字数とともに表示します。
SVGをbase64にしてはいけません
埋め込み画像に関していちばんよくある間違いがこれで、書き出し機能やビルドプラグインが、人と同じくらいの頻度でこれをやります。
SVGはテキストです。URLはもともとテキストを運べます。逃がす必要のある文字はほんのひと握り、%、#、<、>、そして囲んだ引用符だけで、それ以外はまったくそのままにできます。その方式でエンコードすると、同じファイルのbase64よりだいたい5分の1短いURIができますし、しかもそのあと、ノイズではなくテキストとして圧縮されます。
そのうえ、読めるままです。
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E..."); viewBoxが見えます。何もデコードせずに、エディター上で塗りの色を変えられます。同じファイルをbase64にすれば、それは二度と誰も触らない文字の壁になります。画像をデータURIには、SVGだと判明したものに対してこれを自動で行いますし、;base64をどうしても要求するツールチェーン向けに、チェックボックスも用意しています。
SVGのときだけ壊れる引用符の間違い
CSSはurl()を引用符なしで書くことを許していますし、ふつうのファイル名なら、それで問題ありません。
background-image: url(logo.png); 同じことをパーセントエンコードしたSVGでやると、壊れます。引用符のないurl()のトークンは、最初の空白、丸括弧、引用符、制御文字で終わります。そしてSVGは空白だらけです。属性と属性のあいだ、パスの数値と数値のあいだ、どこにでもあります。すると宣言は不正になり、CSSは不正な宣言を黙って捨てるので、背景も出ず、エラーも出ません。
直し方は、毎回引用符を付けることです。
background-image: url("data:image/svg+xml,%3Csvg ... %3E"); これは、エンコーダーが空白を逃がす必要のない理由でもあります。引用符で囲まれたURLの中では空白は完全に合法ですし、ひとつずつ%20にしていたら、ファイル中の空白1つにつき3文字かかります。この2つの判断は、対になっています。URIを引用符で囲めば、空白はそのままにできます。この道具が出すどの形も引用符で囲まれているのは、まさにこの理由です。
メディアタイプは正しくなければなりません
データURIは自分の種類を宣言し、ブラウザはその言葉をそのまま受け取ります。取得したファイルに対して行われるような、中身を見て推測する代替手段はありません。実際はJPEGのものについてimage/pngと言えば、画像は表示されませんし、役に立つ場所にメッセージも出ません。
これが効いてくるのは、拡張子が嘘をつくからです。JPEGとして書き出された写真がlogo.pngという名前になっているのは、ディスクの上でふつうに見つかるものです。一方、画像ファイルの先頭数バイトは、それが何であるかを曖昧さなく述べています。どの形式にも署名があるからです。ですから道具は、名前ではなくファイルを読むべきです。ここの道具はそうしますし、両者が食い違うときには知らせます。
混乱を招く形で失敗するので、知っておく価値のある形式が2つあります。iPhoneが写真を撮る形式であるHEICと、スキャナーが作るTIFFです。どちらも完全に正しいデータURIになりますが、Safari以外のブラウザは、それを描きません。URIが壊れているのではなく、その形式がウェブの対応するものではないだけです。先に変換してください。
公開するつもりのなかったメタデータ
データURIは、ファイルの1バイトずつの複製です。デコードも書き直しも行われませんし、たいていそれこそが目的です。画質が失われないからです。ですが同時に、ファイルの中のそれ以外もすべて付いてくる、ということでもあります。
スマホから出したばかりの写真は、EXIFを抱えています。撮影地のGPS座標、撮影日時、カメラの機種、そしてしばしばそのシリアル番号です。それがファイルの30KBになることもあります。埋め込めば、スタイルシートの中の40KBのbase64となり、すべてのページのクリティカルパスに乗ります。そして自宅の住所が、誰も見ようと思わない形でリポジトリにコミットされます。
先にEXIFビューアー・削除ツールで外してください。画像に触れずに、入れ物だけを書き直します。そのガイドもあります。画像をデータURIにする道具は、JPEG・PNG・WebPにどれだけメタデータがあるのかを読み取り、何かをコピーする前にそれを伝えます。
手に入れたら、どこに置くか
画像が1つのルールにしか出てこないなら、URIをそのルールに置いてください。2つ以上に出てくるなら、カスタムプロパティとして一度だけ宣言してください。アイコンはたいてい2つ以上に出てきます。ホバー時の状態とダークテーマを数えれば、なおさらです。
:root {
--icon-search: url("data:image/svg+xml,%3Csvg ... %3E");
}
.search-field { background-image: var(--icon-search); }
.search-button::before { content: var(--icon-search); } 3KBのURIを4つのルールに貼れば、12KBのスタイルシートと、アイコンを変えるときに直す4か所になります。カスタムプロパティなら、どちらも1つです。テーマ切り替えが成り立つ形でもあります。メディアクエリの中で--icon-searchを定義し直せば、それを使っているすべてが従います。
CSSではなく<img>タグに入れる場合は、widthとheightを書いてください。埋め込まれた画像は即座に読み込まれるので、サイズがないと、見えないほど速く起きるレイアウトのずれが発生しますし、それでも減点はされます。例外はSVGです。viewBoxしか持たないSVGには固有のピクセルサイズがないので、ブラウザの既定である300×150をタグに書き込むと、拡大縮小できる画像を、誰も選んでいない大きさに固定してしまいます。
altは、そこに書くべき本当のことがない限り、空にしておいてください。その画像が意味を運んでいるのか、飾りなのかを知っているのは書く人だけですし、ファイル名から推測した説明は、スクリーンリーダーを使う人にとって、説明がまったくないより悪いからです。
答えが「やめておけ」のとき
エンコード後に50KBを超えるなら、埋め込みは間違った道具であり、エンコードをどれだけ丁寧にやっても直りません。試す価値のある順に、代わりの手を挙げます。
- 小さくします。埋め込むには大きすぎる画像は、たいてい全般的に大きすぎます。画像圧縮ツールは写真を指定したサイズまで持っていきますし、画像リサイズツールはピクセル寸法を、レイアウトが実際に使う分まで落とします。本当の問題は、非常にしばしばそちらです。
- SVGとして描き直す。40KBのPNGとして書き出されたアイコンは、しばしば900バイトのSVGです。これは圧縮の差ではなく形式の差ですし、高精細画面の問題も同時に解決します。
- ファイルのまま残して、先読みさせる。
<link rel="preload" as="image">は、バイト列をクリティカルパスに移さずに、取得をすぐ始めます。埋め込みの利点の大半が得られ、キャッシュの代償はまったくありません。
これにアップロードは要りません
ファイルをbase64にエンコードするのは、計算です。ブラウザが最初から持っている2つの関数、btoaとencodeURIComponentであり、書き方を変えるために画像がサーバーまで往復しなければならない技術的な理由は、一切ありません。これをするためにファイルをアップロードさせる変換サイトは、こちらの理由ではなく、自分の理由でアップロードしています。
ここの道具は、どこへも送りません。ページのContent-Security-Policyには接続してよいアドレスがすべて挙がっていますが、そのどれも当サイトのものではありません。言われるより確かめたい方は、ページを読み込んでから回線を抜き、それでもエンコードしてみてください。オンライン変換サイトにファイルをアップロードしても安全ですか?には、どの道具にも試せる確認方法がさらに3つ載っています。