CSS内の画像埋込で高速化(Data URI)


スタイルシートの中で画像を多数呼出していると、HTTPリクエストが大量発生してページの読み込みが遅くなります。このような場合、CSS Spriteを使って回避することが一般的ですが、Data URIを使うと運用はもっと簡単です。

CSSファイルへの埋込

例えばOSSCafeの場合、サイトのCSS内で16ほどの画像ファイルを読込んでいます。

style/images.css
body { background-image:url(images/body.png) }
body>header { background-image: url(images/header.png) }
body>header div.center>h1 { background-image:url(images/logo.png) }
...()

このCSSファイル内の画像ファイルを、Data URIに置き換えると、

style/images.css?inline
body { background-image:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAEAAAABACAYAAACqaXHeAAAABHNCSVQICAgIfAhkiAAAAAlwSFlzAAAK8AAACvABQqw0mAAAABx0RVh0U29mdHdhcmUAQWRvYmUgRmlyZXdvcmtzIENTM5jWRgMAAAQRdEVYdFhNTDpjb20uYWRvYmUueG1wADw/eHBhY2tldCBiZWdpbj0iICAgIiBpZD0iVzVNME1wQ2VoaUh6cmVTek5UY3prYzlkIj8+Cjx4OnhtcG1ldGEgeG1sbnM6eD0iYWRvYmU6bnM6bWV0YS8iIHg6eG1wdGs9IkFkb2JlIFhNUCBDb3JlIDQuMS1jMDM0IDQ2LjI3Mjk3NiwgU2F0IEphbiAyNyAyMDA3IDIyOjExOjQxICAgICAgICAiPgogICA8cmRmOlJE
...(略)

のように、画像部分がBASE64エンコードで埋め込まれた形になります。

埋め込みの自動化 ("?inline"を付けたときだけ)

ただ、これを手動で実行するのは面倒です。ここでは、CSSファイルの読み込み時に"?inline"とつけた場合のみ、自動的に埋込むことにします。HTML内の表記としてはこんな感じ。

index.html
<link rel="stylesheet" type="text/css" href="images.css?inline" />

ちなみに、想定しているファイル構成は次の通り。

  • index.html
  • style/
    • .htaccess
    • images/ - CSSから参照する画像を格納するディレクトリ
    • images.css - 画像指定のみのCSS
    • other.css - 画像指定以外のCSS
    • embed-images.php - 自動変換スクリプト

.htaccessにてmod_rewriteを使って、"?inline"をつけた場合だけ、PHPのスクリプトを介するよう設定します。後半のFileMatchの部分は、スクリプトの直接起動を制限するためのものです。

style/.htaccess
RewriteEngine On
RewriteCond %{REQUEST_FILENAME} -f
RewriteCond %{QUERY_STRING} inline
RewriteRule \.css$ embed-images.php [QSA,L]

<FilesMatch "embed-images.php">
Order Deny,Allow
Deny from All
Allow from env=REDIRECT_STATUS
</FilesMatch>

Data-URIに置換え

CSSファイル内の画像ファイルを、自動的にData URIに置き換えるPHPスクリプト(embed-images.php)を作成します。といっても、該当するCSSファイルを読込んで、画像ファイル名の部分をData URIに置きかえているだけ。

style/embed-images.php
<?php
$mime_types = array('png'=>'image/png', 'jpg'=>'image/jpeg', 'gif'=>'image/gif');
header('Content-type: text/css');
echo preg_replace_callback(
    '/url\((.*?\.(' . implode('|', array_keys($mime_types)) . '))\)/im',
    function ($matches) use ($mime_types) {
        $url = trim($matches[1], '\'\"');
        return file_exists($url)
            ? 'url(data:' . $mime_types[strtolower($matches[2])] . ';base64,' . base64_encode(file_get_contents($url)) . ')'
            : $matches[0];
    },
    file_get_contents($_SERVER['DOCUMENT_ROOT'] . str_replace('?inline', '', $_SERVER['REQUEST_URI']))
);

メリット / デメリット

冒頭に書いたように、HTTPリクエストが減って、ページの読み込みが高速になるはずです。このような構成を取ることで、運用面でも次のメリットがあります。

  • ?inline を外せば、通常のCSSにすぐ戻せる
  • HTTPサーバがローカルに無い場合でも、元のCSSが表示される

後者は、デザイナがCSSを変更する場合に、ローカルファイルとしてそのまま扱える点で有利です。

Data URIにした場合、BASE64エンコードのため、画像のバイナリデータと比べてファイルサイズが大きくなります。このこと自体は、デメリットなのですが、リクエスト数を減らす方がどちらかというと重要です。また、HTTPサーバからの送出時にgzip圧縮するのであれば、サイズはバイナリとほぼ変わらなくなります。

Appendix

ソースのダウンロード

GitHubにも上げておきました。https://github.com/cognitom/wagon

IEどうするのん?

IEは、バージョン8以降でData URIに対応です。なので、IE7以下をケアする場合は、IEのバージョンで条件分岐。IE7以下で"?inline"を外せばOKです。このあたり、自由にできるのはCompassの"inline-image"より使いやすいところ。

index.html
<!--[if lte IE 7]><link rel="stylesheet" type="text/css" href="images.css" /><![endif]-->
<!--[if gte IE 8]><!--><link rel="stylesheet" type="text/css" href="images.css?inline" /><!--<![endif]-->

他の方法 - Sass

Compass/Sass を使っているのであれば、inline-imageという機能を使って、同様のことが属性単位で可能です。詳しくはこちら

body
    background-image:inline-image(images/body.png)

他の方法 - gulp

追記: gulpを使う方法がオススメです。

  • gulp-base64: CSSに含まれる画像のパスをbase64にエンコード。一定サイズ以下のみエンコードすることも可能。