Lightning Fast Image Previews with Pure CSS and LQIP

Matthias Endler

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

原文は Matthias Endler により に公開されました。 このブログを購読する

Freepikより転用
出典: Freepikより転用

私のウェブサイトはそれなりに高速です。

どの端末から、どこからアクセスしても、すべてのページがキビキビと読み込まれるようにしたいと思っています。驚くことではないかもしれません。なにしろ、ただの素のHTMLとCSSだけで作っているのですから。JavaScriptは可能な限り避けています。

ただ一つ、ずっと気になっていたことがありました。画像が読み込まれたあとにレイアウトがずれることです。

問題は、テキストを表示する準備ができた時点では、まだ画像の寸法がわからないことにあります。そのため、上にある画像が読み込まれた瞬間に、テキストが画面の下方向へ押し出されてしまうのです。

また、画像の読み込み中はプレビューも表示されず、空白がぽっかり空いたままになります。低速な回線では次のように見えます。

スタイルが適用されていないコンテンツが一瞬表示される様子を示したイラスト
スタイルが適用されていないコンテンツが一瞬表示される様子を示したイラスト

画像の幅と高さをハードコーディングすれば直せますが、面倒ですしミスも起こりやすくなります。しかもプレビューは表示されません。そこで、他の人はどうしているのだろうと考えました。🤔

小さなサムネイル画像

ぼんやりと覚えていたのですが、Facebookはモバイルアプリで極小のプレビュー用サムネイルを使っているという話です。JPEGヘッダーから量子化テーブルを抽出してプレビューを描画するそうです。この情報はクライアント側に保存されるため、毎回ダウンロードする必要がありません。残念ながら、この手法は画像エンコーダーを完全に制御できることが前提です。アプリならともかく、ウェブサイトではほとんど使えません。

探し続けました。

そんなとき、同僚のTobias BaldaufLQIP (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は2番目に小さかったのですが、画像をリサイズしてぼかしフィルターをかけたときの仕上がりに満足できませんでした。
最終的には、サイズと品質のバランスに優れたPNGを採用することにしました。zopfli圧縮をサポートするoxipngを使ってさらに最適化し、サムネイルのサイズを約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でラップし、その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;
  }
}

最後のパートはサムネイルの扱いです。他の多くのサイトと同様に、ぼかしフィルターをかけることにしました。どこか画像がfrozen(凍結)したように見えるので、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不要
  • すべてのモダンブラウザで動作
  • メイン画像の読み込みに失敗した場合のフォールバックに対応
  • オーバーヘッドはごくわずか

参考資料

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

コメント