この記事は『まぼろしのJS勉強会 #4 「パフォーマンスアップ道場」』で自分が話したネタをまとめたものです。
基本的には(本記事執筆時点の)半年以上前に "Using SVG as placeholders — More Image Loading Techniques" というブログ記事に書かれていたことを自分でやってみただけなのであまり目新しくはないかも。
やってみたかったや〜つ
やってみたかったのは上記の記事の後半で紹介されている "Silhouettes" の節のやつ。
デモが CodePen で公開されいるので以下に貼ります。
See the Pen Lazy loading with inline SVG by Mikael Ainalem (@ainalem) on CodePen.
見ての通り、プレースホルダーのシルエットからリアルイメージ(っていう言い方で伝われ

とりあえず結果報告
勉強会のテーマがパフォーマンスアップなので、実際に作ったデモでどの程度目的が果たされているかを報告します。
- ブラーイメージからのクロスフェード(Mediumのとは違った実装だけど見かけ的には同じ)
- シルエットからのクロスフェード(今回やってみたかったや〜つ)
ページの初期表示、つまりプレースホルダーが表示されるまでの速度が「2」の方が早いことは一目瞭然です。
「1」の実装としては低解像度画像に CSSのfilter をかけているのだけど、どうしても画像ファイルの容量の分だけ遅くなってしまう。
対して「2」はSVGを用いているので読み込み容量が少なく、おかげで初期表示が速いってわけっす。
容量と読み込み時間の差
実際にページ全体の容量と読み込みにかかる時間がどの程度違うのかを比較してみました。
より純粋な計測のためにレイジーロードの機能をオフにしたバージョンを用意し、ChromeのDeveloperToolでキャッシュ無効・回線速度=Fast3Gの状態で試してみたのが以下です。
全体容量 | 読み込み完了時間 | |
---|---|---|
1 | 333KB | 3.01秒 |
2 | 70.4KB | 1.59秒 |
圧倒的じゃないか我が軍(SVG)は…
SVGによるシルエットのプレースホルダーを採用しない理由がありませんな。
Potrace でラスターをベクターに変換する
何はともあれまずはシルエット画像を生成しなくてはなりません。
超ドMの人ならイラレで元画像からトレースできるかもしれませんが、自分には無理なのでここはプログラムに頼ります。
Potrace - Transforming bitmaps into vector graphics
Potrace はラスターグラフィック(ビットマップ)をベクターグラフィックに変換してくれるCUIのソフトウェアです。
MacならHomebrew、Linuxならそれぞれのパッケージマネージャーでインストール可能です。
Windowsは良く分かんないけどコンパイル済みの一式を配布しているっぽいです。
使い方も簡単で、 potrace
コマンドにBMPファイルとオプションを指定すれば変換結果を標準出力してくれます。
convert path/to/input.jpg bmp:- | potrace -s > path/to/output.svg
元画像がBMPでない場合は適当に変換してから渡します。上記では ImageMagickの convert
コマンドを用いています。
potrace
コマンドの -s
オプションは出力形式にSVGを指定するためのものです。
オプションなしだとEPS、オプションを指定することでSVGの他にもPDFなど様々な形式に出力することが可能です。
出力例はこんな感じ
どれも精密にトレースされていてちょっと感動しちゃいますねー。
しかしこのままプレースホルダーに用いるにはいくつか問題があります。
例えばこちらの画像ですが、なんと容量が407KBもあります。
元のJPGが1024x1024で364KBなので、SVGにすることによってかえって肥大化してしまってます。
初期容量を抑えるためにSVGにしたいのにこれじゃまったく本末転倒じゃん…!
-t
オプションで精密度を調整する
前述の例に示したような、凹凸と陰影の激しいテクスチャーを持った画像を potrace
にかけると大量のパスが生成されるため容量が肥大化してしまいます。
-t
オプションは生成されるパスの量を制限するためのものです。
公式ドキュメントより引用
-t, --turdsize n - suppress speckles of up to this size (default 2)
"speckles" つまり再現する「斑点」の大きさを抑制してくれると、そういうことみたいです。
(ちなみに "turd" は って意味らしい…)。
先ほど407KBにもなってしまった画像を今度は -t 100
で変換してみます。
potrace -s -t 100
結果はこちら
両者を並べてみると凹凸と陰影の再現度が違うのが良く分かります。
生成されるパスの数が大幅に減ったので容量も174KBまで減りました。
しかしプレースホルダーとして用いるにはやはり大きすぎる…。う〜む。
小さい元画像からSVGを生成する
出力例で一覧にした画像はすべて1024x1024です。
元画像の情報量が多ければ生成されるSVGも大きくなるのはまあ必然でしょう。
なので今度は元画像のサイズを小さくしたうえで potrace
にかけてみます。
結果はこちら
元画像を240x240にして、 -t 10
を指定したところ、容量は28KBまで下がりました!
SVGならブラウザで拡大表示することができるので、小さい元画像から生成した方が容量が抑えられて良さそうです。
(ちなみに240x240にした元画像の容量は32KB)
SVGO と gzip でさらに小さく
元画像32KBに対して28KBってあんまり変わんなくね?と思ったそこのあなた。
SVGが本領を発揮するのはここからです。
SVGO でコードを最適化する
potrace
が出力するSVGのソースコードは人間様に忖度したプリティーなコードになっています。
なので無駄な改行や空白文字を取り除くことで容量を削減することができるのです。
SVGOを使えば簡単かつ安全にコードの最適化が可能です。
gzip に感謝する
SVGは画像であると同時にテキストです。
また potrace
が出力するSVGのソースコードはそのほとんどが数字で構成されているため重複文字が大量にあります。
なのでgzipによる恩恵を受けやすいのです。
今の一般的なウェブサーバーであればgzipはまず有効なハズなので、ネットワーク上ではローカル環境で確認できる容量よりも遙かに小さい容量でやりとりされます。
SVGOMG で最適化と gzip プレビューする
SVGOMGはブラウザ上でSVGOを実行し、その結果を gzip 適用後の容量とともに出力してくれる凄い人です。
試しにこのSVGファイルを最適化してみましょう。
なんということでしょう
ローカルでは28KBあったファイルがコードの最適化と gzip よって10.21KBまで圧縮されるではないですか!
これならプレースホルダー用の画像として用いても容量的な問題は全くないと言えそうです。
-c
オプションで塗りの色を指定する
ちょっと話が前後しますが、ここまでの出力例では塗りの色は黒でした。
プレースホルダー用の画像として用いるにはちょっと色が濃すぎます。
-C
オプションは塗りの色を指定するためのものです。
公式ドキュメントより引用
-C, --color #rrggbb - set foreground color (default black)
今回のデモでは塗りの色を #cccccc
にしてみました。
potrace -s -t 10 -C "#cccccc"
最終的な出力結果がこちらです。
Potrace いろんな実装あるんだな〜
PotraceはCUIソフトウェアであると紹介しましたが、実は他にも色々な実装があるみたいです。
冒頭のブログ記事でも紹介されていますが、なんとJSとPHPの実装まであります。
JSの実装があるということは、例えば Potrace した結果を(Node実装である) SVGO にかけて返すような API を Lambda と API Gateway で簡単に作ることもできそうです。
S3に画像を置くとシルエットのSVGを勝手に作ってくれる、とかもアリですね!
うは、夢がひろがりんぐww
SVGをプレースホルダーに用いることの課題
ここまで盛り上げておいて何ですが、SVGをプレースホルダーに用いることについて課題もあります。
それはアクセシビリティ対策と SEO です。
「ブラーイメージ + CSSフィルター」の実装の場合、低解像度とは言え一応「本来の画像」を読み込んではいるので、例えばJS無効のUAや検索エンジンのロボットにも最低限の情報を伝えることができます。
ところがシルエットのSVGをプレースホルダーにすると、上記のような環境に最低限の情報を届けることができなくなってしまうのです。
この点についてはまだ自分は解決策を持っておらず、何か良いアイディアをお持ちの方がいらっしゃれば是非教えていただきたいです…!
レイジーロードの実装について
今回の記事ではレイジーロード機能の実装自体については特に触れません。
まあライブラリは山ほどあるし、今は IntersectionObserver 使えば簡単に実装できるから…。
以上です!!