そうだ、WebPを使おう

この記事は公開されてから2年以上経過しています。 情報が古い可能性がありますのでご注意ください。

Googleから WebP が公開されたのは2010年9月30日らしいので、本稿執筆時点で10年以上が経過していることになります。メジャーなモダンブラウザのほとんどがサポートするようにもなり、そろそろスタンダードに利用して良い頃合いではないかと思うのです。

WebP とはなにか

ナチュラルに書き出してしまいましたが、まず WebP とは何物なのでしょう。

A new image format for the Web | WebP | Google Developers

WebP is a modern image format that provides superior lossless and lossy compression for images on the web. Using WebP, webmasters and web developers can create smaller, richer images that make the web faster.

WebP は優れた可逆・非可逆圧縮を備えた静止画フォーマットで、Web体験をより速くする事を目的としています。雑に紹介すると、PNGやJPEGといった従来フォーマットよりもファイルサイズが小さく、かつ美麗な静止画ファイルを提供できる画像フォーマットだそうです。

なお、WebPの発音は wikipedia によれば “weppy” (ウェッピィ)らしいとされています。

なお2011年頃まで、Googleは「WebP is pronounced “weppy”」(WebPは”weppy”と発音する)と説明していたが、2020年現在その記述は削除されている。
── https://ja.wikipedia.org/wiki/WebP

何故削除されたのかは定かでありませんが、GIF のように争わずに済みそうですね。

対応環境

気になるWebPが使用できる環境ですが、現時点ではメジャーなブラウザのほとんどで対応されています。IEとSafariの一部で難があるようですが、 img 要素で表示させる分には問題は生じないでしょう。(後述)

Data on support for the webp feature across the major browsers from caniuse.com

WebP を使おう

WebPは、JPGやPNG、GIFのような画像フォーマットのひとつです。使い方といっても一部の環境を除いて特別な事はなく、 <img /> 要素で出してやれば良いでしょう。

<img src="images/photo.webp" width="300" height="200" alt="写真" />

しかし、InternetExploerやMacOS 10.x系のSafariではWebPを描画出来ないため、その為の対応が必要になります。具体的には、 <picture> 要素を使います。例えばこのように。

<picture>
  <source srcset="images/photo.webp" type="image/webp" />
  <img src="images/photo.png" width="300" height="200" alt="写真" />
</picture>

<picture><source> を用いる事で、 image/webp に対応したブラウザで閲覧した場合のみ、 <source> の指定が有効になって images/photo.webp が描画されます。対応していない場合は、スキップされて images/photo.png が描画されるでしょう。

なお、IEについてはこの <picture> にすでに対応していないそうですが、その場合は中の <img> が描画されるだけなので、結果オーライという事になりますね。

CSS の background で使う方法

CSSには <picture> の様な分岐方法が備わっていないため、基本的にIE等のためのフォールバックは出来ません。従来どおり、省サイズ化したPNG画像などで対応するのが無難でしょう。

一応 Modernizr 等で <html> 要素にクラスを付与させることで分岐するという方法もあるにはありますが、そうまでして背景画像にWebPを使いたいわけではないという個人的な思いがあるので、(そもそも転送量を減らす為にWebPを使いたいのにそれを使うために別途リソースを読まなければならないのは本末転倒では?)ここでその方法には触れないでおきます。それよりは、CSS背景でなく <img /> で表現出来るようにマークアップ・スタイリングした方がより建設的だと考えるのです。

WebP を作ろう

WebP形式の画像出力はそれなりの数のアプリケーションがサポートしていますが未だ網羅されているとは言い難く、一部はプラグインの導入なしには出力することが出来ません。ですので、ここは安心と信頼の node.js に手伝ってもらうことにします。

画像を扱うパッケージは沢山ありますが、今回は imagemin とそのプラグインでやってみましょう。事前に必要なパッケージを追加しておきます。

$ yarn add -D imagemin imagemin-webp

単一のディレクトリに出力する

単一のディレクトリに出力する

まずは、入力・出力ディレクトリが多階層になっていない単一ディレクトリである場合。これはとても簡単です。次のような内容で imagemin.js 的なファイルを作成し、 node imagemin.js のように実行します。

const imagemin = require('imagemin')
const imageminWebp = require('imagemin-webp')

imagemin(['src/*.png'], { plugins: [imageminWebp()], destination: 'dist' })

src 直下にある png ファイルを全て WebP に変換して、出力先に指定している dist ディレクトリに保存します。スクリプトの置き場所や入力・出力ディレクトリはこのままだと使いづらいと思うので、お好みにあわせて変えてやってください。

ディレクトリ構造を維持しつつ出力する

ディレクトリ構造を維持しつつ出力する

次は、入力ディレクトリが多階層になっていて、出力先でも同じ構造を維持したいケースですが、これは少々厄介です。というのも、本稿執筆時点で imagemin がディレクトリ構造を維持する機能を持ち合わせていないのです。

関連ページ :

当然この機能を待ち望んでいる人は多くいらっしゃる様なのですが、ざっとログを読んでみたところ、開発者はあまりこの機能に積極的ではなく、PR主も最早興味を失ってしまった様な感想をいだきました。現在進行系でコメントがついていたりとまだPRは生きている様子ではありますが、ともかく、現時点では imagemin のオプションに頼って実現する事はできないということ。

一例として、都度出力先のパスを計算して指定してやる方式でなんとか実現できました。

const fs = require('fs')
const path = require('path')
const imagemin = require('imagemin')
const imageminWebp = require('imagemin-webp')

const srcRoot = './src'
const distRoot = './dist'

imagemin(
  [`${srcRoot}/**/*.png`],
  { plugins: [imageminWebp()] }
).then((files) => {
  files.forEach((file) => {
    // 入力パスから出力パスを計算
    const distPath = file.sourcePath.replace(srcRoot, distRoot)
    // 出力先ディレクトリを作成
    fs.mkdirSync(
      path.dirname(distPath),
      { recursive: true }
    )
    // ファイル書き出し
    fs.writeFileSync(
      // 出力パスの拡張子を .webp に変更
      distPath.replace(path.extname(distPath), '.webp'),
      file.data
    )
  })
})

src 配下にある全ての .png ファイルを走査して、ディレクトリ構造はそのままに dist に出力します。 fs.mkdirSync() にいつのまにか追加されていた recursive オプションのお陰でとても楽が出来ました。ありがとう、 fs

imagemin のオプションで自在に制御出来るようになったあかつきには、このコードは笑顔でかなぐり捨てましょう。

ついでにPNGも圧縮しよう

ついでにPNGも圧縮しよう

前述の通りIEとSafariの一部ではWebPを扱うことが出来ず、従来の画像形式でフォールバックする必要があります。せっかく imagemin を導入していることですし、ついでにPNGの圧縮も一緒に出来るようにしてしまいましょう。

imagemin でPNGの圧縮を行うために、 imagemin-pngquant をインストールしておきます。

$ yarn add -D imagemin-pngquant

「ディレクトリ構造を維持しつつ出力する」 で前述したコードに pngquant で圧縮する処理を追加しただけです。出力先ディレクトリの存在確認と生成はWebP作成時にもう済んでいるので、 async/await で同期処理にすれば pngquant の処理においては不要になります。

const fs = require('fs')
const path = require('path')
const imagemin = require('imagemin')
const imageminWebp = require('imagemin-webp')
const imageminPngquant = require('imagemin-pngquant')

const srcRoot = './src'
const distRoot = './dist'

;(async () => {
  await imagemin(
    [`${srcRoot}/**/*.png`],
    { plugins: [imageminWebp()] }
  ).then((files) => {
    files.forEach((file) => {
      const distPath = file.sourcePath.replace(srcRoot, distRoot)
      fs.mkdirSync(
        path.dirname(distPath),
        { recursive: true }
      )
      fs.writeFileSync(
        distPath.replace(path.extname(distPath), '.webp'),
        file.data
      )
    })
  })

  await imagemin(
    [`${srcRoot}/**/*.png`],
    { plugins: [imageminPngquant()] }
  ).then((files) => {
    files.forEach((file) => {
      fs.writeFileSync(
        file.sourcePath.replace(srcRoot, distRoot),
        file.data
      )
    })
  })
})()

これで、ディレクトリ構造を保持しつつ、WebP画像と圧縮済PNGが出力出来るようになりました。おめでとうございます。 🎉

入力ディレクトリと出力ディレクトリが別である理由

WebPを生成するだけであればその限りではありませんが、PNG画像の軽量化も同時に行う場合、入力ディレクトリと出力ディレクトリは分けておくのが良いでしょう。

というのも、今回使用している pngquant が行うのはロスレス圧縮ではないので、同じファイルに繰り返し上書き処理を行うと、どんどん画質が劣化していきます。処理済みのPNG画像にさらに圧縮をかけてしまうことのないように、明示的に保存場所を分けておきたくなるでしょう。いまや JS/CSS がそうであるように、画像もまた開発用と公開用とでファイルを分けて考えると良いと思います。

ロスレスの optiPNG を利用する手もありますが、こちらは肝心の圧縮効率が pngquant ほどは高くありません。また、画質の劣化は防げるとはいえ、再度の処理が無駄であるということに変わりはないので、何らかの理由で optiPNG を採用する場合でも、別々に管理するのが吉と言えますね。

まとめ

WebP は非常に優秀で、手元にあった 2.4MB のPNG画像を WebP に変換したところ、123KB まで小さくなり、pngquant で圧縮した結果の 734KB に対して大きなアドバンテージがありました。(いずれも引数に何も渡さないデフォルトの設定です)しかも綺麗。

どういう仕組かはわたしにはさっぱりわかりませんが、モバイル端末が幅を効かせているこの状況で、活用しない手はないですね。