レイジーロードイメージのプレースホルダーにSVGを用いてみる

この記事は[『まぼろしのJS勉強会 #4 「パフォーマンスアップ道場」』](https://maboroshi.connpass.com/event/87409/)で自分が話したネタをまとめたものです。  
基本的には（本記事執筆時点の）半年以上前に ["Using SVG as placeholders — More Image Loading Techniques"](https://jmperezperez.com/svg-placeholders/) というブログ記事に書かれていたことを自分でやってみただけなのであまり目新しくはないかも。  

## やってみたかったや〜つ
やってみたかったのは[上記の記事](https://jmperezperez.com/svg-placeholders/)の後半で紹介されている "Silhouettes" の節のやつ。
デモが CodePen で公開されいるので以下に貼ります。
<p data-height="265" data-theme-id="0" data-slug-hash="aLKxjm" data-default-tab="result" data-user="ainalem" data-embed-version="2" data-pen-title="Lazy loading with inline SVG" data-preview="true" class="codepen">See the Pen <a href="https://codepen.io/ainalem/pen/aLKxjm/">Lazy loading with inline SVG</a> by Mikael Ainalem (<a href="https://codepen.io/ainalem">@ainalem</a>) on <a href="https://codepen.io">CodePen</a>.</p>
<script async src="https://static.codepen.io/assets/embed/ei.js"></script>
見ての通り、プレースホルダーのシルエットからリアルイメージ（っていう言い方で伝われ :pray: ）にクロスフェードする感じ。  
[Medium](https://medium.com/)でおなじみのブラーイメージ（ぼけぼけに滲ませたアレ）からクロスフェードするUIにもそろそろ飽きたし、SVGを使うならイニシャルのダウンロード容量を抑えられるのでは、ということで実際にやってみた！ :muscle: 

## とりあえず結果報告
勉強会のテーマがパフォーマンスアップなので、実際に作ったデモでどの程度目的が果たされているかを報告します。  
1. [ブラーイメージからのクロスフェード](https://develop--hardcore-hopper-5f54d9.netlify.com/static-image.html)（Mediumのとは違った実装だけど見かけ的には同じ）
2. [シルエットからのクロスフェード](https://develop--hardcore-hopper-5f54d9.netlify.com/external-svg.html)（今回やってみたかったや〜つ）

ページの初期表示、つまりプレースホルダーが表示されるまでの速度が「2」の方が早いことは一目瞭然です。
「1」の実装としては低解像度画像に [CSSのfilter](https://developer.mozilla.org/en-US/docs/Web/CSS/filter) をかけているのだけど、どうしても画像ファイルの容量の分だけ遅くなってしまう。
対して「2」はSVGを用いているので読み込み容量が少なく、おかげで初期表示が速いってわけっす。  

### 容量と読み込み時間の差
実際にページ全体の容量と読み込みにかかる時間がどの程度違うのかを比較してみました。
より純粋な計測のためにレイジーロードの機能をオフにしたバージョンを用意し、ChromeのDeveloperToolでキャッシュ無効・回線速度=Fast3Gの状態で試してみたのが以下です。
1. [ブラーイメージ](https://hardcore-hopper-5f54d9.netlify.com/static-image.html)
![スクリーンショット 2018-06-04 16.09.58.png](https://qiita-image-store.s3.amazonaws.com/0/36521/08b47494-8a2e-ef00-d9e0-c018ca74649a.png)  
2. [シルエット](https://hardcore-hopper-5f54d9.netlify.com/external-svg.html)
![スクリーンショット 2018-06-04 16.10.25.png](https://qiita-image-store.s3.amazonaws.com/0/36521/9f2e2ff2-6787-948d-eefc-ea36c005b2ec.png)  

| |全体容量|読み込み完了時間|
|-----|-----|-----|
|1|333KB|3.01秒|
|2|70.4KB|1.59秒|

圧倒的じゃないか我が軍（SVG）は… :innocent: 
SVGによるシルエットのプレースホルダーを採用しない理由がありませんな。

## **Potrace** でラスターをベクターに変換する
何はともあれまずはシルエット画像を生成しなくてはなりません。
超ドMの人ならイラレで元画像からトレースできるかもしれませんが、自分には無理なのでここはプログラムに頼ります。  
[Potrace - Transforming bitmaps into vector graphics
](http://potrace.sourceforge.net/)  
**Potrace** はラスターグラフィック（ビットマップ）をベクターグラフィックに変換してくれるCUIのソフトウェアです。
MacならHomebrew、Linuxならそれぞれのパッケージマネージャーでインストール可能です。
Windowsは良く分かんないけどコンパイル済みの一式を配布しているっぽいです。  
使い方も簡単で、 `potrace` コマンドにBMPファイルとオプションを指定すれば変換結果を標準出力してくれます。  

```sh
convert path/to/input.jpg bmp:- | potrace -s > path/to/output.svg
```
元画像がBMPでない場合は適当に変換してから渡します。上記では [ImageMagick](http://www.imagemagick.org/script/index.php)の `convert` コマンドを用いています。
`potrace` コマンドの `-s` オプションは出力形式にSVGを指定するためのものです。
オプションなしだとEPS、オプションを指定することでSVGの他にもPDFなど様々な形式に出力することが可能です。  
[出力例はこんな感じ](https://hardcore-hopper-5f54d9.netlify.com/convert-sample.html)  
どれも精密にトレースされていてちょっと感動しちゃいますねー。  
しかしこのままプレースホルダーに用いるにはいくつか問題があります。
例えば[こちらの画像](https://hardcore-hopper-5f54d9.netlify.com/23757224659_e9dc6bf61b_b.svg)ですが、なんと容量が407KBもあります。
元のJPGが1024x1024で364KBなので、SVGにすることによってかえって肥大化してしまってます。
初期容量を抑えるためにSVGにしたいのにこれじゃまったく本末転倒じゃん…！ :cold_sweat:

### `-t` オプションで精密度を調整する
前述の例に示したような、凹凸と陰影の激しいテクスチャーを持った画像を `potrace` にかけると大量のパスが生成されるため容量が肥大化してしまいます。
`-t` オプションは生成されるパスの量を制限するためのものです。

[公式ドキュメント](http://potrace.sourceforge.net/)より引用
> -t, --turdsize n           - suppress speckles of up to this size (default 2)

"speckles" つまり再現する「斑点」の大きさを抑制してくれると、そういうことみたいです。
（ちなみに "turd" は :poop: って意味らしい…）。

先ほど407KBにもなってしまった画像を今度は `-t 100` で変換してみます。

```sh
potrace -s -t 100
```
[結果はこちら](https://hardcore-hopper-5f54d9.netlify.com/23757224659_e9dc6bf61b_b_t100.svg) 
両者を並べてみると凹凸と陰影の再現度が違うのが良く分かります。
生成されるパスの数が大幅に減ったので容量も174KBまで減りました。  
しかしプレースホルダーとして用いるにはやはり大きすぎる…。う〜む。

### 小さい元画像からSVGを生成する
[出力例](https://hardcore-hopper-5f54d9.netlify.com/convert-sample.html)で一覧にした画像はすべて1024x1024です。
元画像の情報量が多ければ生成されるSVGも大きくなるのはまあ必然でしょう。
なので今度は元画像のサイズを小さくしたうえで `potrace` にかけてみます。
[結果はこちら](https://hardcore-hopper-5f54d9.netlify.com/23757224659_e9dc6bf61b_m_c-none.svg)
元画像を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](https://jakearchibald.github.io/svgomg/)はブラウザ上でSVGOを実行し、その結果を gzip 適用後の容量とともに出力してくれる凄い人です。
試しに[このSVGファイル](https://hardcore-hopper-5f54d9.netlify.com/23757224659_e9dc6bf61b_m_c-none.sv)を最適化してみましょう。
![capture_svgomg.jpg](https://qiita-image-store.s3.amazonaws.com/0/36521/a05799c6-a8d3-aa01-54f7-6cdce5045fa6.jpeg)
なんということでしょう :open_mouth:
ローカルでは28KBあったファイルがコードの最適化と gzip よって10.21KBまで圧縮されるではないですか！  
これならプレースホルダー用の画像として用いても容量的な問題は全くないと言えそうです。

### `-c` オプションで塗りの色を指定する
ちょっと話が前後しますが、ここまでの出力例では塗りの色は黒でした。
プレースホルダー用の画像として用いるにはちょっと色が濃すぎます。
`-C` オプションは塗りの色を指定するためのものです。

[公式ドキュメント](http://potrace.sourceforge.net/)より引用
> -C, --color #rrggbb        - set foreground color (default black)

今回のデモでは塗りの色を `#cccccc` にしてみました。

```sh
potrace -s -t 10 -C "#cccccc"
```

[最終的な出力結果がこちら](https://hardcore-hopper-5f54d9.netlify.com/23757224659_e9dc6bf61b_m.svg)です。

## **Potrace** いろんな実装あるんだな〜
PotraceはCUIソフトウェアであると紹介しましたが、実は他にも色々な実装があるみたいです。
[冒頭のブログ記事](https://jmperezperez.com/svg-placeholders/)でも紹介されていますが、なんとJSとPHPの実装まであります。
JSの実装があるということは、例えば Potrace した結果を（Node実装である） SVGO にかけて返すような API を Lambda と API Gateway で簡単に作ることもできそうです。 :muscle: 
S3に画像を置くとシルエットのSVGを勝手に作ってくれる、とかもアリですね！
うは、夢がひろがりんぐｗｗ :apple: :apple: :apple: 

## SVGをプレースホルダーに用いることの課題
ここまで盛り上げておいて何ですが、SVGをプレースホルダーに用いることについて課題もあります。
それはアクセシビリティ対策と SEO です。
「ブラーイメージ + CSSフィルター」の実装の場合、低解像度とは言え一応「本来の画像」を読み込んではいるので、例えばJS無効のUAや検索エンジンのロボットにも最低限の情報を伝えることができます。
ところがシルエットのSVGをプレースホルダーにすると、上記のような環境に最低限の情報を届けることができなくなってしまうのです。
この点についてはまだ自分は解決策を持っておらず、何か良いアイディアをお持ちの方がいらっしゃれば是非教えていただきたいです…！ :bow: 

## レイジーロードの実装について
今回の記事ではレイジーロード機能の実装自体については特に触れません。
まあライブラリは山ほどあるし、今は IntersectionObserver 使えば簡単に実装できるから…。
以上です！！