Pure CSSとLQIPによる超高速な画像プレビュー
私のウェブサイトはそこそこ高速です。
どんなデバイスや場所からでも、ページの読み込みがキビキビと感じられるようにしたいと思っています。驚くことではないでしょう。なにしろ、ただの素のHTMLとCSSだけで作っているのですから。JavaScriptは可能な限り使わないようにしています。
ただ一つ、本当に気になっていたことがありました。画像の読み込み後にレイアウトがガタッとずれる(リフローする)ことです。
問題は、テキストを表示する準備ができた時点では、画像のサイズがまだわからないことです。そのため、上にある画像が読み込まれた途端、テキストが画面の下へ押し下げられてしまいます。
また、画像の読み込み中はプレビューもなく、ただの空白が表示されるだけです。回線が遅い環境では次のように見えます。

画像の幅と高さをハードコードすれば修正できますが、それでは手間がかかりますし、ミスも起こりやすくなります。しかもプレビューは表示されません。そこで、他の人たちはどうしているのだろうと考えてみました。🤔
小さなサムネイル画像
ぼんやりと、Facebookがモバイルアプリで小さなプレビュー用サムネイルを使っていることを思い出しました。Facebookはモバイルアプリで小さなプレビュー用サムネイルを使っています。JPEGヘッダーから量子化テーブルを抜き出してプレビューを描画する手法です。この情報はクライアント側に保存されるため、毎回ダウンロードする必要がありません。残念ながら、この手法は画像エンコーダーを完全に制御できることが前提です。アプリならうまくいきますが、ウェブサイトではほとんど使えません。
探索は続きました。
そして同僚のTobias Baldaufが、LQIP(Low-Quality Image Placeholders)を教えてくれました。
アイデアは次のとおりです。
- インラインで埋め込んだ低品質なサムネイル画像を含めてページを読み込みます。
- ページが完全に読み込まれたら(例えば
onloadイベントが発火したタイミングで)、高品質な画像を遅延読み込みします。
残念ながら、この手法はJavaScriptを必要とします。それでもアイデア自体は気に入ったので、さまざまな画像サイズやフォーマットで実験を始めました。目標は、標準的な画像フォーマットを使って、できる限り小さいサムネイルを作ることでした。
ベンチマーク
異なるファイル形式でエンコードした、幅15ピクセルのサムネイルがこちらです。

サムネイルの作成にはいくつかのツールを使いました。JPEGとPNGのエンコードにはsvgexportを使いました。
svgexport img.svg img.png "svg{background:white;}" 15: 1%WebPにはcwebpを使いました。
cwebp img.png -o img.webpGIFはオンラインツールで変換し、gifsicleで最適化しました。
gifsicle -O3 < img.gif > img_mini.gif比較
WebPが最も小さくなりましたが、すべてのブラウザでサポートされているわけではありません。
GIFが次に小さかったのですが、画像をリサイズしてぼかしフィルターをかけたときの仕上がりに満足できませんでした。
最終的には、サイズと品質のバランスが非常に優れていたPNGに落ち着きました。さらにoxipngを使って画像を最適化しました。これはzopfli圧縮に対応しています。これにより、サムネイルのサイズは300〜400バイト程度に収まりました。
サムネイル作成の処理は、ブログのビルドツールチェーンに組み込みました。画像を生成する実際のコードはかなり地味なものです。本当に見たいという方は、Githubで公開しています。
JavaScriptを使わない方法
画像プレビュー用のHTMLの骨組みは次のとおりです。
<figure>
<div class="loader">
<object data="image.svg" type="image/svg+xml"></object>
<img class="frozen" src="data:image/png;base64,..." />
</div>
</figure>コツは、フルサイズの画像とプレビュー画像の両方をloaderというdivでラップし、width: autoというCSSを指定することです。
.loader {
position: relative;
overflow: hidden;
width: auto;
}SVGはimg要素ではなくobjectタグでラップしています。こうすることで、SVGを読み込めなかった場合にプレースホルダーを表示できます。objectはloaderのdivの左上に配置します。
.loader object {
position: absolute;
}
.loader img,
.loader object {
display: block;
top: 0;
left: 0;
width: 100%;
}プレースホルダーのハックは次のとおりです。参考リンクも併記します。
/* https://stackoverflow.com/a/29111371/270334 */
/* https://stackoverflow.com/a/32928240/270334 */
object {
position: relative;
float: left;
display: block;
&:after {
position: absolute;
top: 0;
left: 0;
display: block;
width: 1000px;
height: 1000px;
content: "";
background: #efefef;
}
}最後のパートはサムネイルの扱いです。他の多くのサイトと同様に、ぼかしフィルターをかけることにしました。どこか画像が凍りついたように見えるので、CSSセレクターはそう名付けました。さらに、境界をシャープに見せるために拡大のトランスフォームも適用しています。
.frozen {
-webkit-filter: blur(8px);
-moz-filter: blur(8px);
-o-filter: blur(8px);
-ms-filter: blur(8px);
filter: blur(8px);
transform: scale(1.04);
animation: 0.2s ease-in 0.4s 1 forwards fade;
width: 100%;
}
@keyframes fade {
0% {
opacity: 1;
}
100% {
opacity: 0;
}
}JavaScriptの代わりにCSSアニメーションを使っています。
アニメーションの長さは、ページを訪れたすべてのユーザーの読み込み時間の95パーセンタイルを基に決めています。あくまで概算ですが、ほとんどの読者にはうまく機能するはずです。
結果
- JavaScriptは不要です
- すべてのモダンブラウザで動作します
- メイン画像を読み込めなかった場合のフォールバックに対応しています
- オーバーヘッドはごくわずかです
参考リンク
記事をランダムに読む