Gulp 5で画像容量を圧縮

更新:2025-01-18
JavaScript

概要

PNG・JPG・SVGの容量をGulp 5で圧縮する。以下の構成を想定。

/project-dir
├── dist
│ ├── ここに圧縮後の画像を生成
│ └── hoge
│ └── ディレクトリ構造は維持して生成
└── src
├── ここに元画像を配置
└── hoge
└── ディレクトリも切れるようにする

プロジェクトの作成

package.jsonを適当に作成。

/project-dir/package.json
{
"name": "gulp-compress-image",
"private": true,
"version": "0.0.0",
"type": "module",
"scripts": {},
"devDependencies": {}
}

開発環境の整備

モジュールの追加

/project-dir
npm i -D del gulp gulp-imagemin imagemin-pngquant svgo through2
モジュール名簡易説明
delディレクトリやファイルの削除。
gulpタスクランナー。
gulp-imagemin画像圧縮プラグイン。
imagemin-pngquantPNG専用の画像圧縮プラグイン。
svgoSVGの最適化。
through2カスタムストリーム作成用。

gulp-imageminあたりは脆弱性があると表示されるが、ローカルで使用するだけなので問題なし。

Node.jsのバージョン

どうもNode.jsがバージョン21より新しいとエラーになる。

/project-dir
Error [ERR_REQUIRE_ASYNC_MODULE]: require() cannot be used on an ESM graph with top-level await. Use import() instead. To see where the top-level await comes from, use --experimental-print-required-tla.
at ModuleJobSync.runSync (node:internal/modules/esm/module_job:393:13)
at ModuleLoader.importSyncForRequire (node:internal/modules/esm/loader:335:47)
at loadESMFromCJS (node:internal/modules/cjs/loader:1570:24)
at Module._compile (node:internal/modules/cjs/loader:1722:5)
at Object..js (node:internal/modules/cjs/loader:1905:10)
at Module.load (node:internal/modules/cjs/loader:1474:32)
at Function._load (node:internal/modules/cjs/loader:1286:12)
at TracingChannel.traceSync (node:diagnostics_channel:322:14)
at wrapModuleLoad (node:internal/modules/cjs/loader:234:24)
at Module.require (node:internal/modules/cjs/loader:1496:12) {
code: 'ERR_REQUIRE_ASYNC_MODULE'
}

バージョン21で固定する。

/project-dir
volta install node@21.7.3
volta pin node@21.7.3

バージョンを確認してみるとよく分からないエラーに遭遇。

/project-dir
node -v
Error: proto::tool::required
× This project requires Node.js 21.7.3 (detected from .../package.json), but this version has
not been installed. Install it with proto install node 21.7.3, or enable the auto-install setting to automatically install missing
versions!

書いてあるとおりproto install node 21.7.3したら解消した。

雑記

出力先の掃除

delを使用して過去に生成した画像を消すようにした。置いておく意味もないので。

画像が壊れる問題

Gulp 5の場合、srcのencodingを無効にする必要があるらしい。
https://stackoverflow.com/questions/78730067/gulp-imagemin-breaking-images-and-not-optimizing

PNGの圧縮

PNGはgulp-imageminoptipngでも圧縮できるみたいだが、そこで処理が止まってしまう?ので、かわりにimagemin-pngquantを使用した。

SVGの圧縮

imageminsvgo()でだいぶ圧縮されるが、iddata-nameといった属性が残っていた。

sample.svg
<?xml version="1.0" encoding="UTF-8"?>
<svg id="_レイヤー_2" data-name="レイヤー 2" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 800 800">
...
</svg>

svgo()のオプションを指定する方法がよく分からなかったので、直接svgoをインストールして使うことにした。

圧縮率の表示

through2を使用して、圧縮率をコンソールに出力するようにした。

/project-dir
- jpg/sample.jpg 953293 bytes -> 602399 bytes 36%削減
- png/sample.png 5726530 bytes -> 1908340 bytes 66%削減
- svg/sample.svg 774 bytes -> 496 bytes 35%削減

コンソールの色付けは以下を参考にした。
https://qiita.com/shuhei/items/a61b4324fd5dbc1af79b

成果

https://github.com/namakurajin/compress-image-with-gulp-5

/project-dir/gulpfile.js
// 必要なモジュール
import { src, dest } from 'gulp';
import imagemin, { mozjpeg, svgo } from 'gulp-imagemin';
import pngquant from 'imagemin-pngquant';
import { optimize } from 'svgo';
import through2 from 'through2';
import { deleteSync } from 'del';
// コンソールの色付け
var green = '\u001b[32m';
var reset = '\u001b[0m';
export default () => (
deleteSync(['dist/**']), // まず、出力先を空にしておく
src('src/**/*.{jpg,png,svg}', { encoding: false })
// 画像を圧縮
.pipe(
imagemin([
mozjpeg({quality: 40, progressive: true}),
pngquant({
quality: [0.5, 0.6],
speed: 1
}),
svgo()
])
)
// SVGからdata-name属性を除去
.pipe(through2.obj((file, _enc, cb) => {
const result = optimize(file.contents.toString(), {
plugins: [
{
name: 'removeAttrs',
params: {
attrs: ['data-name']
}
}
]
});
file.contents = Buffer.from(result.data);
cb(null, file);
}))
// 圧縮率を表示
.pipe(through2.obj((file, _enc, cb) => {
const originalSize = file.stat.size;
const compressedSize = file.contents.length;
const compressionRatio = Math.floor((originalSize - compressedSize) / originalSize * 100);
console.log(` - ${green} ${file.relative} ${reset} ${originalSize} bytes -> ${compressedSize} bytes ${green} ${compressionRatio}%削減 ${reset}`);
cb(null, file);
}))
// 出力
.pipe(dest('dist'))
);

続く...かもね