🦁

Web制作のための画像圧縮とWebP生成

2022/04/28に公開約6,600字

4 月が終わり、GW に突入しますね。
季節の変わり目になりクーラーのタイミングを伺ってる、スピッカートの金山(@spicato_kana)です。

今回は、弊社で使用している Web 制作のための画像圧縮と WebP 生成を紹介します!

毎回 Web サービスなどを用いて圧縮すると効率が悪いので、弊社では自動化しています。

圧縮と WebP 生成には、 sharp というライブラリを使用しています。
sharp-cli があり webpack 上で簡単に使用できるのですが、ここ一年以上更新がされていないので今回は使用しません。

https://sharp.pixelplumbing.com/

かわりに、npm script で実行します。

結論(コード)

最初に結論を書くのがいいと聞くので、最初にコード全体を書きます。

package.json
"sharp:watch": "onchange \"src/assets/images/**/*.{png,jpg,jpeg,svg,gif}\" -- node sharp-watch.js {{changed}}",
sharp-watch.js
const fs = require("fs");
const path = require("path");
const sharp = require("sharp");

let dirName = path.dirname(process.argv[2]);
let fileName = path.basename(process.argv[2]);

let outPutDir = `dist${dirName.replace("src", "")}`;

// 拡張子を取得
function getExtension(file) {
  let ext = path.extname(file || "").split(".");
  return ext[ext.length - 1];
}
const fileFormat = getExtension(fileName);

(() => {
  // もしディレクトリがなければ作成
  if (!fs.existsSync("dist/assets/images")) {
    fs.mkdirSync("dist/assets/images");
  }
  // サブディレクトリがなければ作成
  if (!fs.existsSync(outPutDir)) {
    fs.mkdirSync(outPutDir);
  }

  let sh = sharp(`${dirName}/${fileName}`);
  let webp = sharp(`${dirName}/${fileName}`);

  if (fileFormat === "jpg" || fileFormat === "jpeg") {
    sh = sh.jpeg({ quality: 70 });
    webp = webp.webp({ quality: 70 });
  } else if (fileFormat === "png") {
    sh = sh.png({ quality: 70 });
    webp = webp.webp({ quality: 70 });
  } else if (fileFormat === "gif") {
    sh = sh.gif({ quality: 70 });
    webp = webp.webp({ quality: 70 });
  } else if (fileFormat === "svg") {
    // svgは複製のみ
    fs.copyFile(process.argv[2], `${outPutDir}/${fileName}`, (err) => {
      if (err) {
        fs.unlinkSync(`${outPutDir}/${fileName}`);
        console.log(`${fileName}${outPutDir}から削除しました。`);
        return;
      }
      console.log(`${fileName}${outPutDir}に複製しました。`);
    });
    return;
  } else {
    console.log("対応していないファイル形式です。");
    return;
  }

  sh.toFile(`${outPutDir}/${fileName}`, (err, info) => {
    if (err) {
      // 該当ファイルがない場合はdistから削除
      if (fs.existsSync(`${outPutDir}/${fileName}`)) {
        fs.unlinkSync(`${outPutDir}/${fileName}`);
        fs.unlinkSync(`${outPutDir}/webp/${fileName.replace(/\.[^/.]+$/, ".webp")}`);
        console.log(`${fileName}${outPutDir}から削除しました。`);
      }
      return;
    }
    console.log(`${fileName}を圧縮しました。 ${info.size / 1000}KB`);

    // webp生成、もしディレクトリがなければ作成
    if (!fs.existsSync(`${outPutDir}/webp`)) {
      fs.mkdirSync(`${outPutDir}/webp`);
    }
    webp.toFile(`${outPutDir}/webp/${fileName.replace(/\.[^/.]+$/, ".webp")}`, (err, info) => {
      console.log(`${fileName}をwebpに変換しました。 ${info.size / 1000}KB`);
    });
  });
})();


解説

それでは、簡単な解説をしていきます!
まずは、必要なパッケージをインストールします。

npm install -D sharp onchange

onchange は、ファイルの変更を監視するためのパッケージです。


package.json
"sharp:watch": "onchange \"src/assets/images/**/*.{png,jpg,jpeg,svg,gif}\" -- node sharp-watch.js {{changed}}",

{{ changed }} に変更したファイルのパスが渡されます。

jpeg,jpg,png,gif は圧縮と webp 生成を行い、svg は dist へ複製のみを実施します!


sharp-watch.js
const fs = require("fs");
const path = require("path");
const sharp = require("sharp");

まず、Node.js の pathfs、モジュール をインストールします。
詳しくは下記をご覧ください。
今回は、パスの読み込みとファイルの生成で使用しています。

https://nodejs.org/api/path.html
https://nodejs.org/api/fs.html
sharp-watch.js
let dirName = path.dirname(process.argv[2]);
let fileName = path.basename(process.argv[2]);

let outPutDir = `dist${dirName.replace("src", "")}`;

// 拡張子を取得
function getExtension(file) {
  let ext = path.extname(file || "").split(".");
  return ext[ext.length - 1];
}
const fileFormat = getExtension(fileName);

ここでは、ディレクトリ名、ファイル名、アウトプット先、拡張子を取得しています。
process.argv[2] は、コマンドライン引数で渡されたファイルのパスです。


package.json
"node sharp-watch.js {{changed}}",

上記だと、node が process.argv[0] , sharp-watch.js が process.argv[1] , {{changed}} が process.argv[2] となっています。


sharp-watch.js
  // もしディレクトリがなければ作成
  if (!fs.existsSync("dist/assets/images")) {
    fs.mkdirSync("dist/assets/images");
  }
  // サブディレクトリがなければ作成
  if (!fs.existsSync(outPutDir)) {
    fs.mkdirSync(outPutDir);
  }

  let sh = sharp(`${dirName}/${fileName}`);
  let webp = sharp(`${dirName}/${fileName}`);

ディレクトリが無ければ作成します。
さらに sharp をインスタンス化します。


sharp-watch.js
  if (fileFormat === "jpg" || fileFormat === "jpeg") {
    sh = sh.jpeg({ quality: 70 });
    webp = webp.webp({ quality: 70 });
  } else if (fileFormat === "png") {
    sh = sh.png({ quality: 70 });
    webp = webp.webp({ quality: 70 });
  } else if (fileFormat === "gif") {
    sh = sh.gif({ quality: 70 });
    webp = webp.webp({ quality: 70 });
  } else if (fileFormat === "svg") {
    // svgは複製のみ
    fs.copyFile(process.argv[2], `${outPutDir}/${fileName}`, (err) => {
      if (err) {
        fs.unlinkSync(`${outPutDir}/${fileName}`);
        console.log(`${fileName}${outPutDir}から削除しました。`);
        return;
      }
      console.log(`${fileName}${outPutDir}に複製しました。`);
    });
    return;
  } else {
    console.log("対応していないファイル形式です。");
    return;
  }

if 文で拡張子毎に処理を分岐します。
quality で画質調整(圧縮率)を個別にすることができます。
svg は複製のみを実施しています。


sharp-watch.js
  sh.toFile(`${outPutDir}/${fileName}`, (err, info) => {
    if (err) {
      // 該当ファイルがない場合はdistから削除
      if (fs.existsSync(`${outPutDir}/${fileName}`)) {
        fs.unlinkSync(`${outPutDir}/${fileName}`);
        fs.unlinkSync(`${outPutDir}/webp/${fileName.replace(/\.[^/.]+$/, ".webp")}`);
        console.log(`${fileName}${outPutDir}から削除しました。`);
      }
      return;
    }
    console.log(`${fileName}を圧縮しました。 ${info.size / 1000}KB`);

    // webp生成、もしディレクトリがなければ作成
    if (!fs.existsSync(`${outPutDir}/webp`)) {
      fs.mkdirSync(`${outPutDir}/webp`);
    }
    webp.toFile(`${outPutDir}/webp/${fileName.replace(/\.[^/.]+$/, ".webp")}`, (err, info) => {
      console.log(`${fileName}をwebpに変換しました。 ${info.size / 1000}KB`);
    });
  });

ここで圧縮と WebP 生成を行います。
さらに、画像を削除した場合は dist からも削除します。


まとめ

Web サイトにおけるスピードアップについて、最近より関心が高くなっていると思います。
そこで画像の圧縮や新しい拡張子(WebP,AVIF など)を使用して、Web サイトのスピードアップを実現することができます。

Webpack を使用して、imagemin-webpack-plugin 等パッケージで圧縮することも可能ですが、ビルド時間が膨大になることが多く sharp を用いて開発体験を向上させることができました。
詳しく比較ができればいいのですが、面倒なので今回は割愛します!
是非、ご自身の手で試してみてください(笑)!

参考

https://youtu.be/js_2iTLQFvI
https://blog.kozakana.net/2019/04/sharp-output-format/

Discussion

ログインするとコメントできます