Lightning Fast Image Previews with Pure CSS and LQIP

Matthias Endler

Pure CSSとLQIPによる超高速な画像プレビュー

Freepikより改変
出典:Freepikより改変

私のウェブサイトはそこそこ高速です。

どんなデバイスや場所からでも、ページの読み込みがキビキビと感じられるようにしたいと思っています。驚くことではないでしょう。なにしろ、ただの素の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.webp

GIFはオンラインツールで変換し、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を読み込めなかった場合にプレースホルダーを表示できます。objectloaderの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は不要です
  • すべてのモダンブラウザで動作します
  • メイン画像を読み込めなかった場合のフォールバックに対応しています
  • オーバーヘッドはごくわずかです

参考リンク

原文は Matthias Endler により に公開されました。

この記事は「muse-spark-1.2-contributor」を使用して翻訳されました。