ユニファ開発者ブログ

ユニファ株式会社プロダクトデベロップメント本部メンバーによるブログです。

サムネイルは libvips + WebP で生成するのがよいか?

こんにちは、rightgo09 です。今回は libvips + WebP での簡単な検証結果を確認します。

libvips とは

高速かつ低メモリな画像変換ライブラリです。
Rails の ActiveStorage の画像リサイズ(自分で指定する必要ありますが)や、AWS Lambda に標準で搭載されるなど、いろいろな場面で ImageMagick ではなく libvips が使用されるようになっているようです。

ref. Why is libvips quick · libvips/libvips Wiki · GitHub

WebP とは

比較的新しい画像フォーマットです。
IE のサポートが 2022 年 6 月に終了したため、ほぼすべてのブラウザで対応済みとなりました。以前よりは安心して使用可能な状況と言えそうです。

AWS ブログによる紹介記事でも採用されている

aws.amazon.com

こちらのブログでは、libvips で WebP に変換する内容が紹介されています。一例ではありますが、AWS としてもこの組み合わせは価値があるとみなしているのかもしれません。

※ Node.js の sharp ライブラリが libvips を使用している
※ リクエストの Accept ヘッダが許していれば WebP に変換している

sharp

sharp - npm

上述の AWS ブログでも採用されている、libvips を使用した Node.js のライブラリ「sharp」では

Resizing an image is typically 4x-5x faster than using the quickest ImageMagick and GraphicsMagick settings due to its use of libvips.

とあります。すごい。

簡単に検証してみた

事前に用意できない、例えばユーザがアップロードした画像を動的にサムネイルにして表示させる場合を想定し、以下を確認します。

  • 処理時間
  • ファイルサイズ
  • 画質

JPEG 画像を変換する

1枚目の画像 (JPEG) (1798x2398) (2,051,477 bytes)

2枚目の画像 (JPEG) (4032x3024) (3,401,442 bytes)

変換する

  • 1枚目の画像 → 500 x 667 ( 28% )
  • 2枚目の画像 → 504 x 378 ( 12.5% )

結果

2枚目の写真 quality: 30 を指定したときのまとめ

quality を 30 にすると、JPEG ではガビガビしたところが出てきましたが、WebP ではまだまともなサムネイルのままのように見えます。

まとめと所感

  • クオリティを低くしたとき、ともに処理時間は短くなる傾向にあり、ファイルサイズは減る
  • クオリティを低くしたとき、ファイルサイズが同じくらいなら JPEG よりも WebP の方がきれいに見える
  • クオリティの指定をしないとき、libvips の処理時間は ImageMagick よりも早いしファイルサイズは小さいし、見た目で違いはわからない
  • クオリティの指定をしないとき、WebP はファイルサイズが小さくなるし処理速度も早い

処理速度と見た目の良さを考慮すると、libvips で WebP を使っていく(クオリティは要模索)のがユーザの満足面、インフラのコスト面ともに良さそうなのかな、という簡単な検証結果でした。

Web サービスは高速化の時代なので、いかに CDN にキャッシュを載せていくか、CDN からのラストワンマイルでいかに運ぶ量を減らすか、ということで、今回のようないかに小さいサイズで満足できる画質を提供できるかといった模索は継続して必要のようです。

計測コード

環境は MacBook Pro (2018) Intel Core i7 2.2GHz 16GB メモリ

ImageMagick の方は Ruby バインディングの RMagick で、libvips の方は Node.js の sharp で変換した際の処理時間を内部で計測しました。Node.js の ImageMagick のライブラリがうまく使えず、Ruby にて ImageMagick の計測を行いました。言語による違いが処理速度に含まれていますが、画像変換の時間に比べれば小さいものだろうと判断して今回の調査として使用しました。

生成画像のファイルサイズが小さいほど最後の書き込み時間が有利になりますが、それも込みで計測しています。

また sharp での JPEG → JPEG への変換に mozjpeg を使用すると、処理時間は 2 倍程度長くなるものの生成画像のサイズが何割か小さくなる、という結果になりました。

ImageMagick
starting = Process.clock_gettime(Process::CLOCK_MONOTONIC)
image = Magick::Image.read(src_path).first
image.format = format
if format == "WEBP" && quality != "default"
  image.define("webp:emulate-jpeg-size", true)
end
image.resize!(w, h)
if quality == "default"
  image.write(dest_path)
else
  image.write(dest_path) { |i| i.quality = quality }
end
ending = Process.clock_gettime(Process::CLOCK_MONOTONIC)
elapsed = (ending - starting) * 1000
size = Magick::Image.read(dest_path).first.filesize
puts "#{src_path}\tRMagick\t#{format}(Quality #{quality})\t#{elapsed.round(5)} ms\t#{size} bytes"
libvips
const hrstart = process.hrtime();
let info;
if (format === "jpeg") {
  if (quality === "default") {
    info = await sharp(src_path).resize(w, h).toFile(dest_path);
    // info = await sharp(src_path).jpeg({ mozjpeg: true }).resize(w, h).toFile(dest_path);
  } else {
    info = await sharp(src_path).jpeg({ quality: quality }).resize(w, h).toFile(dest_path);
    // info = await sharp(src_path).jpeg({ mozjpeg: true, quality: quality }).resize(w, h).toFile(dest_path);
  }
} else if (format === "webp") {
  if (quality === "default") {
    info = await sharp(src_path).toFormat(format).resize(w, h).toFile(dest_path);
  } else {
    info = await sharp(src_path).toFormat(format).webp({ quality: quality }).resize(w, h).toFile(dest_path);
  }
}
const hrend = process.hrtime(hrstart);
const ms = hrend[1] / 1000000;
console.info(`${src_path}\tSharp\t${format}(Quality ${quality})\t${ms} ms\t${info['size']} bytes`);

unifa-e.com