imageminを利用して大量の画像を一気に圧縮して最適化!
画像を Web で表示する用に一括して圧縮するための方法として、imageminを使います。 これで Lighthouse で圧縮不足が指摘されることはなくなります。 ※ただし、画像のスケールなど他の問題は別途解決の必要があります。
バックグラウンド
ブログや Web アプリケーションを作っていると、どうしても画像を使う必要があります。これをしないと、Web ページを表示させる時に画像のダウンロードがボトルネックになってしまってページが表示されずに離脱を招く恐れもあります。5G の時代が来ればいいのですが、利用者全員が 5G の恩恵に預かれるわけではないので、当面は画像の最適化は必要なタスクとなります。
画像を探すことはもちろん、その後に適切な大きさに切り取り・スケールダウンし、最後に圧縮を行います。 圧縮は色々なツールがソフトウェアやオンラインツールとして提供されていますが、一長一短があります。
画像を総合的に扱う場合、Photoshop の右に出るものはありませんが、有償ツールのため個人で使うには大きめの決断が必要であったり、企業で使うにしても、稟議をどうやって通そうかと悩むことになります。ただ一度使うとその便利さ、効率の良さに抜け出すのは難しくなるでしょう。
Photoshop は昔から高価なツールでした。そこで無料で使える Photoshop ライクな製品として、GIMP は20年も以上前から存在しています。使用感は当初に比べると格段に改善していますが、Photoshop と比較するとどうしても劣ってしまいます。私は画像に対して高度な作業をする必要はありませんが、圧縮だけはします。ところが、GIMP で圧縮した場合、どうしても Lighthouse のスコアを満たすようなファイルを簡単に作ることができませんでした。 これが今回、この記事の内容を作ることになった理由です。
- TinyPng
検索するとこのあたりが Google の上位に出てきます。他にもたくさんのサイトがあります。 サイトによってはリサイズまで出来たりもします。圧縮率はサイト次第です。 オンラインツールを使うにあたって特に気になることはアップロードした画像の取り扱いになります。フリーの画像であれば特に意識する必要はありませんが、自分自身に著作権が帰属する画像や購入した画像、社外秘となる画像など特別な画像には配慮が必要となります。利用規約を読むことである程度どのようになるのかは知ることができますが、その通りに運用されているのかどうかは別問題です。ほとんどのサイトは大手企業が受けるような監査は受けていないので、どこを信じるかは利用者に任されている状況になります。
自分自身の手元の環境(ローカル環境)で実行できるプログラムも多々あります。もちろん自分自身で0から画像を取り扱うプログラムを書いても楽しいとは思いますが、勉強以外であればすぐに利用できるツールが望ましいでしょう。 その中で imagemin は長く利用されて安定しており、また開発も継続しているので安心できるライブラリになります。
自分の環境の中であれば、画像を流出するようなこともないですし、パラメータの変更や加工も自在にできたり、他のプログラムと組み合わせれば自動化も可能になり、メリットは大きいです。
imagemin のインストール
NodeJS のインストール
nodejs.orgでインストーラをダウンロードしてインストールをしてください。推奨版であるバージョン 12.18.0 LTS がおすすめです。
作業フォルダーを決めます。
ここではC:/home/test/compressionとします。
コマンドプロンプトを開いて、上記の場所に移動します。
cd C:/home/test/compression
プロジェクトを作成
内容はそのままで OK です。後からいつでも変更可能です。
npm init -y
imagemin のインストール
npm install -D imagemin imagemin-jpegtran imagemin-pngquant
- imagemin は本体です。
- imagemin-jpegtran は JPEG ファイルを扱うためのプラグインです。
- imagemin-pngquant は PNG ファイルを扱うためのプラグインです。
- 他にも svg, gif, webp など豊富なプラグインが存在ます。こちらで探してみてください。
プログラムの作成
プロジェクト直下に demo01.js というファイルを作って、以下の内容をコピー&ペーストしてください。 ここの例だと、絶対パスはC:/home/test/compression/demo01.jsとなります。
const fs = require("fs"); const path = require("path"); const imagemin = require("imagemin"); const imageminJpegtran = require("imagemin-jpegtran"); const imageminPngquant = require("imagemin-pngquant"); (async () => { const files = await imagemin(["demo/*.{jpg,png,JPG,PNG}"], { destination: "dest", plugins: [ imageminJpegtran(), imageminPngquant({ quality: [0.6, 0.8], }), ], }); console.log(files); })();
画像の準備
- demoフォルダーを作成して、その中に JPEG 画像や PNG 画像をプロジェクト直下に置いてください。何枚でも可能です。
- 出力先がない場合は、自動的に生成されます。
プログラムの実行
node demo01.js
実行すると、以下のような文字が出力されます。
[{data: <Buffer 89 50 4e …>, destinationPath: 'build/images/foo.jpg'}, …]
さらにdestフォルダーの中には圧縮されたファイルが元のファイルと同じ名前で存在します。
なお、destinationを入力ファイルと同じフォルダー(demo)にすると、元の画像が上書きされます。
ここまでで、基本的な内容は終わりです。
アドバンス
再帰的にフォルダーを探索して画像を圧縮する。
上記のプログラムの場合、同じフォルダーにあるファイルのみが対象となりました。そうではなく、そのフォルダー以下にある全てのファイルを対象にしたい場合はどうすればいいでしょうか。
1つの方法は、入力ファイルを指定する箇所を"demo/**/*.{jpg,png,JPG,PNG}"と変更することです。
const files = await imagemin(["demo/**/*.{jpg,png,JPG,PNG}"], {
destination: "dest",
plugins: [
// ...
],
});
これにより現在のフォルダー以下にある全ての JPEG と PNG ファイルが処理対象になります。この指定方法は glob 形式です。
この方法の問題点は圧縮されたファイルが全てdemoフォルダーに入ることです。元のフォルダー構造は維持されません。
そこで1フォルダーずつ処理を回すことで、destinationをそれぞれ指定していきます。
あるフォルダー以下のパスを再帰的に取得する。 この結果は["demo", "demo/001", "demo/002"]という配列になります。
const getDirRecursively = (dir) => { const getChildrenRecursively = (dir) => { // Get child directories under the dir. const readdir = fs .readdirSync(dir, { withFileTypes: true }) .filter((d) => d.isDirectory()); if (readdir.length === 0) { // There're no directories under the dir. return dir; } else { // Get directories recursively. Require >=Node11 return readdir .map((p) => getChildrenRecursively(`${dir}/${p.name}`)) .flat(); } }; return [dir, ...getChildrenRecursively(dir)]; };
それぞれのフォルダーごとに圧縮を実施する。さらに後でリネームできるようにfilesに圧縮後の結果を配列として保持させます。ここで重要なのは、destDirを作っていることです。
(async () => { const files = []; const sourceDirs = getDirRecursively("demo"); for (let inDir of sourceDirs) { // Make output path: demo/001 => dest/demo/001 const destDir = "dest/" + inDir; const filesInDir = await imagemin([`${inDir}/*.{jpg,png,JPG,PNG}`], { destination: destDir, plugins: [ imageminJpegtran(), // @ts-ignore imageminPngquant({ quality: [0.6, 0.8], }), ], }); files.push(...filesInDir); } })();
最後に圧縮したファイル名を変更します。ここではxxx.jpgをxxx.min.jpgというようにします。
files.map((file) => { const ext = path.extname(file.destinationPath); const filename = path.basename(file.destinationPath, ext); const newPath = path.join( path.dirname(file.destinationPath), `${filename}.min${ext}` ); fs.renameSync(file.destinationPath, newPath); });
長くなりましたが、全体としては以下のようになります。demoフォルダー以下を再帰的に探索して、画像を圧縮します。圧縮したファイルはdest/demoフォルダー以下に出力されます。
const fs = require("fs"); const path = require("path"); const imagemin = require("imagemin"); const imageminJpegtran = require("imagemin-jpegtran"); const imageminPngquant = require("imagemin-pngquant"); const getDirRecursively = (dir) => { const getChildrenRecursively = (dir) => { // Get child directories under the dir. const readdir = fs .readdirSync(dir, { withFileTypes: true }) .filter((d) => d.isDirectory()); if (readdir.length === 0) { // There're no directories under the dir. return dir; } else { // Get directories recursively. Require >=Node11 return readdir .map((p) => getChildrenRecursively(`${dir}/${p.name}`)) .flat(); } }; return [dir, ...getChildrenRecursively(dir)]; }; (async () => { const files = []; const sourceDirs = getDirRecursively("demo"); console.log("sourceDirs", sourceDirs); for (let inDir of sourceDirs) { // Make output path: demo/001 => dest/demo/001 const destDir = "dest/" + inDir; const filesInDir = await imagemin([`${inDir}/*.{jpg,png,JPG,PNG}`], { destination: destDir, plugins: [ imageminJpegtran(), // @ts-ignore imageminPngquant({ quality: [0.6, 0.8], }), ], }); files.push(...filesInDir); } // Rename file files.map((file) => { const ext = path.extname(file.destinationPath); const filename = path.basename(file.destinationPath, ext); const newPath = path.join( path.dirname(file.destinationPath), `${filename}.min${ext}` ); fs.renameSync(file.destinationPath, newPath); }); })();
最後に
これだけで色々なことができそうですね。 IE や Safari の問題がありますが、Webp に変換することも簡単です。
それでは!