Web制作のための画像圧縮とWebP生成
4 月が終わり、GW に突入しますね。
季節の変わり目になりクーラーのタイミングを伺ってる、スピッカートの金山(@spicato_kana)です。
今回は、弊社で使用している Web 制作のための画像圧縮と WebP 生成を紹介します!
毎回 Web サービスなどを用いて圧縮すると効率が悪いので、弊社では自動化しています。
圧縮と WebP 生成には、 sharp というライブラリを使用しています。
sharp-cli があり webpack 上で簡単に使用できるのですが、ここ一年以上更新がされていないので今回は使用しません。
直近(2022/12/14 時点)で、更新されて v4 になっていました!
かわりに、npm script で実行します。
結論(コード)
最初に結論を書くのがいいと聞くので、最初にコード全体を書きます。
// ES Modules 対応にする
// Webpack 使用時だといろいろ面倒なので非推奨
"type": "module",
"scripts" : {
"sharp:watch": "onchange \"src/assets/images/**/*.{png,jpg,jpeg,svg,gif}\" -- node sharp-watch.mjs {{changed}}",
}
import fs from 'fs';
import path from 'path';
import sharp from '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(
`\u001b[1;33m ${fileName}を${outPutDir}から削除しました。`
);
return;
}
console.log(
`\u001b[1;32m ${fileName}を${outPutDir}に複製しました。`
);
});
return;
} else {
console.log('\u001b[1;31m 対応していないファイル形式です。');
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(
`\u001b[1;33m ${fileName}を${outPutDir}から削除しました。`
);
}
return;
}
console.log(
`\u001b[1;32m ${fileName}を圧縮しました。 ${info.size / 1000}KB`
);
// ファイル名に『no-webp』が含む場合は webp を生成しない。
if (!fileName.includes('no-webp')) {
// webp生成、もしディレクトリがなければ作成
if (!fs.existsSync(`${outPutDir}/webp`)) {
fs.mkdirSync(`${outPutDir}/webp`);
}
webp.toFile(
`${outPutDir}/webp/${fileName.replace(/\.[^/.]+$/, '.webp')}`,
(err, info) => {
console.log(
`\u001b[1;32m ${fileName}をwebpに変換しました。 ${
info.size / 1000
}KB`
);
}
);
}
});
})();
解説
それでは、簡単な解説をしていきます!
まずは、必要なパッケージをインストールします。
npm install -D sharp onchange
onchange は、ファイルの変更を監視するためのパッケージです。
"sharp:watch": "onchange \"src/assets/images/**/*.{png,jpg,jpeg,svg,gif}\" -- node sharp-watch.mjs {{changed}}",
{{ changed }} に変更したファイルのパスが渡されます。
jpeg,jpg,png,gif は圧縮と webp 生成を行い、svg は dist へ複製のみを実施します!
import fs from 'fs';
import path from 'path';
import sharp from 'sharp';
まず、Node.js の path、fs、モジュール をインストールします。
詳しくは下記をご覧ください。
今回は、パスの読み込みとファイルの生成で使用しています。
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] は、コマンドライン引数で渡されたファイルのパスです。
"node sharp-watch.mjs {{changed}}",
上記だと、node が process.argv[0] , sharp-watch.mjs が process.argv[1] , {{changed}} が process.argv[2] となっています。
// もしディレクトリがなければ作成
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 をインスタンス化します。
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(
`\u001b[1;33m ${fileName}を${outPutDir}から削除しました。`
);
return;
}
console.log(
`\u001b[1;32m ${fileName}を${outPutDir}に複製しました。`
);
});
return;
} else {
console.log('\u001b[1;31m 対応していないファイル形式です。');
return;
}
if 文で拡張子毎に処理を分岐します。
quality で画質調整(圧縮率)を個別にすることができます。
svg は複製のみを実施しています。
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(
`\u001b[1;33m ${fileName}を${outPutDir}から削除しました。`
);
}
return;
}
console.log(
`\u001b[1;32m ${fileName}を圧縮しました。 ${info.size / 1000}KB`
);
// ファイル名に『no-webp』が含む場合は webp を生成しない。
if (!fileName.includes('no-webp')) {
// webp生成、もしディレクトリがなければ作成
if (!fs.existsSync(`${outPutDir}/webp`)) {
fs.mkdirSync(`${outPutDir}/webp`);
}
webp.toFile(
`${outPutDir}/webp/${fileName.replace(/\.[^/.]+$/, '.webp')}`,
(err, info) => {
console.log(
`\u001b[1;32m ${fileName}をwebpに変換しました。 ${
info.size / 1000
}KB`
);
}
);
}
});
ここで圧縮と WebP 生成を行います。
さらに、画像を削除した場合は dist からも削除します。
また、ファイル名に no-webp
がある場合は Webp の生成をスキップします。
全ての画像に実行したいとき
全部の画像にまとめて圧縮と WebP 生成を行いたい場合は下記を実行します。
内容はだいたい似たようなことをしているので省略します。
GitHub からダウンロードしてきた時にビルドに実行したりします。
"sharp:all": "node sharp/sharp-all.mjs"
import fs from 'fs';
import path from 'path';
import sharp from 'sharp';
let dirName = 'src/assets/images';
// 拡張子を確認
function getExtension(file) {
let ext = path.extname(file || '').split('.');
return ext[ext.length - 1];
}
const readSubDir = (folderPath, finishFunc) => {
// フォルダ内の全ての画像の配列
let result = [];
let execCounter = 0;
const readTopDir = (folderPath) => {
execCounter += 1;
fs.readdir(folderPath, (err, items) => {
if (err) {
console.log(err);
}
items = items.map((itemName) => {
return path.join(folderPath, itemName);
});
items.forEach((itemPath) => {
if (fs.statSync(itemPath).isFile()) {
result.push(itemPath);
}
if (fs.statSync(itemPath).isDirectory()) {
//フォルダなら再帰呼び出し
readTopDir(itemPath);
}
});
execCounter -= 1;
if (execCounter === 0) {
if (finishFunc) {
finishFunc(result);
}
}
});
};
readTopDir(folderPath);
};
//サブディレクトリの列挙 非同期
readSubDir(dirName, (items) => {
items.forEach((item) => {
const pathName = path.dirname(item);
const fileName = path.basename(item);
const fileFormat = getExtension(fileName);
let outPutDir = `dist/${pathName.replace('src', '')}`;
// もしディレクトリがなければ作成
if (!fs.existsSync('dist/assets/images')) {
fs.mkdirSync('dist/assets/images');
}
// もしディレクトリがなければ作成
if (!fs.existsSync(outPutDir)) {
fs.mkdirSync(outPutDir);
}
if (fileFormat === '') {
console.log(
`\u001b[1;31m 対応していないファイルです。-> ${fileName}`
);
return;
} else if (fileFormat === 'svg') {
// svgは複製のみ
fs.copyFile(item, `${outPutDir}/${fileName}`, (err) => {
if (err) {
return;
}
console.log(
`\u001b[1;32m ${fileName}を${outPutDir}に複製しました。`
);
});
return;
}
let sh = sharp(`${pathName}/${path.basename(item)}`);
let webp = sharp(`${pathName}/${path.basename(item)}`);
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 {
console.log(
`\u001b[1;31m 対応していないファイルです。-> ${fileName}`
);
return;
}
sh.toFile(`${outPutDir}/${fileName}`, (err, info) => {
if (err) {
console.error(err);
return;
}
console.log(
`\u001b[1;32m ${fileName}を圧縮しました。 ${info.size / 1000}KB`
);
// ファイル名に『no-webp』が含む場合は webp を生成しない。
if (!fileName.includes('no-webp')) {
// webp生成、もしディレクトリがなければ作成
if (!fs.existsSync(`${outPutDir}/webp`)) {
fs.mkdirSync(`${outPutDir}/webp`);
}
webp.toFile(
`${outPutDir}/webp/${fileName.replace(
/\.[^/.]+$/,
'.webp'
)}`,
(err, info) => {
if (err) {
console.error(err);
return;
}
console.log(
`\u001b[1;32m ${fileName}をwebpに変換しました。 ${
info.size / 1000
}KB`
);
}
);
}
});
});
});
まとめ
Web サイトにおけるスピードアップについて、最近より関心が高くなっていると思います。
そこで画像の圧縮や新しい拡張子(WebP,AVIF など)を使用して、Web サイトのスピードアップを実現することができます。
Webpack を使用して、imagemin-webpack-plugin 等パッケージで圧縮することも可能ですが、ビルド時間が膨大になることが多く sharp を用いて開発体験を向上させることができました。
詳しく比較ができればいいのですが、面倒なので今回は割愛します!
是非、ご自身の手で試してみてください(笑)!
参考
Discussion