LCL Engineers' Blog

夜行バス比較なび(高速バス比較)・格安移動・バスとりっぷを運営する LCLの開発者ブログ

npm scriptsで画像圧縮を自動化した際の課題と検討事項

フロントエンドエンジニアの岡田です。 LCLではフロントエンドエンジニアがマークアップも担当しており、画像の圧縮もエンジニアが行っています。 画像圧縮、面倒ですよね。。 いままでいくつか自動化を試しましたが、どれも長くは続きませんでした。

【画像圧縮作業の変遷】

Webpackでimagemin導入

突然動かなくなりTinyPNGで圧縮してしのぐ

面倒なのでMacのAutomator&フォルダアクションで設定
こちらの記事を参考にさせていただきました)

メンバーが増えると共有できず… 結局TinyPNGを使う

画像が圧縮されているかは、GitHubのプルリクエストでは確認しにくいこともあり、どうにかして自動化したいと思っていました。

そこへ同じチームのメンバーからLIGさんの記事を教えていただき試してみまして、本格運用することになりました。 liginc.co.jp

本格運用にあたって、課題や検討したことがありましたので、ご紹介します。 また、最後に実際の設定もご紹介します。 (LIGさんのコードをベースに一部変えています)

LCLで導入するにあたっての課題・検討したこと

imageminでは画像のディレクトリ構成を保つことができない

imageminではディレクトリ構造に配置したファイルも圧縮することはできるのですが、出力先が1階層になってしまいます。

オリジナル画像を以下の構造で配置しても

src
├── images
│   ├── icon
│   │   └── arrow.png
│   └── title
│       ├── heading1.png
│       └── heading2.png

imageminの出力先は以下のようになります。

dest
├── images
│   ├── arrow.png
│   ├── heading1.png
│   └── heading2.png

そこで、imagemin-keep-folder パッケージを導入しました。 www.npmjs.com

imagemin-keep-folderを使うと、ディレクトリ構造そのままで書き出せます。

dest
├── images
│   ├── icon
│   │   └── arrow.png
│   └── title
│       ├── heading1.png
│       └── heading2.png

オリジナル画像をどこに保存し、どこに出力するか

以前Webpackでimageminを導入した際には、圧縮後のファイルだけをGit管理して使っていましたが、使いにくく感じていました。 オリジナル画像をGit管理しないと以下のような点で面倒でした。

  • 画像を保存する時に他のファイルが見えないため、ファイル名の重複に気づけなかったり、他のファイルとは違う規則で命名してしまったりする
  • 階層構造がある場合、ディレクトリを作るのが面倒
    たとえば、/src/images/common/banner/ に保存したい場合、Gitをクローンした時点ではディレクトリが存在しないため、自分でディレクトリを作らないといけません。複数人で作業していたり階層構造が深い場合はひと手間です。

そこで、これを機に圧縮前のファイルをGit管理することにしました。 npmの管理下にsrcディレクトリを作成し、今までとは違うディレクトリに出力します。 出力先を既存と同じにすることもできるのですが、既存の画像はGit管理しないことにしたため分けました。

オリジナル画像保存場所:
/frontend/src/images/ 以下

出力先:
/public/images/◯◯/ 以下

圧縮はサーバでデプロイ時にするか、ローカルでコミット前にするか

画像の圧縮をサーバでデプロイ時にする場合、パフォーマンスや実行時間を考えると、GitHubのプルリクエスト中にある画像だけが圧縮できると良いです。 ただ、今回の仕組みではできないようでした。

サーバでデプロイ時に圧縮する場合は、オリジナル画像の保存場所にある全部の画像を圧縮することになります。 試しに夜行バス比較なびの画像を全部圧縮したところ、デプロイが約1分長くなってしまいました。 また、デプロイ時にサーバの負荷も上がるため、現状の構成では難しそうでした。

そこで、ひとまずローカルで圧縮をすることにして、しばらくは圧縮前後の画像2ファイルをGit管理するという運用にしました。 書き出したファイルも含めてGit管理する運用は最初にReactを導入したときにも採用していましたが、あまり良い方法ではないため、早期に見直したいと思っています。 今後、サーバーのデプロイ手法を変える計画もあり、その際にはサーバー側で圧縮できるように変更する予定です。

watch設定

以上のことから当面は各自のローカル環境で圧縮することにしたため、画像を保存する度に圧縮処理を実行させる必要がでてきました。 imageminにはwatchオプションが用意されていないようなので、onchangeを使いました。 以下のように設定しています。

  "scripts": {
    "imagemin:build": "node imagemin.js",
    "imagemin:watch": "onchange './src/images' -e '**/*.DS_Store' -- npm run imagemin:build"
  },

LCLで使っている設定詳細

以上を踏まえた最終型は以下のとおりです。

パッケージ

devDependencies

imagemin
imagemin-keep-folder
imagemin-mozjpeg
imagemin-pngquant
imagemin-gifsicle
imagemin-svgo
npm install --save-dev imagemin imagemin-keep-folder imagemin-mozjpeg imagemin-pngquant imagemin-gifsicle imagemin-svgo

npm scripts

package.jsonから該当箇所のみ抜粋

  "scripts": {
    "start": "npm run imagemin:watch",
    "imagemin:build": "node imagemin.js",
    "imagemin:watch": "onchange './src/images' -e '**/*.DS_Store' -- npm run imagemin:build"
  },

設定ファイル

imagemin.js

const imagemin = require('imagemin-keep-folder');
const imageminMozjpeg = require('imagemin-mozjpeg');
const imageminPngquant = require('imagemin-pngquant');
const imageminGifsicle = require('imagemin-gifsicle');
const imageminSvgo = require('imagemin-svgo');

imagemin(['src/images/**/*.{jpg,png,gif,svg}'], {
  plugins: [
    imageminMozjpeg({ quality: 80 }),
    imageminPngquant({ quality: '65-80' }),
    imageminGifsicle(),
    imageminSvgo()
  ],
  replaceOutputDir: output => {
    return output.replace(/images\//, '../../public/images/')
  }
}).then(() => {
  console.log('Images optimized');
});

感想

npm scriptsで導入できるのは手軽で良いと思いました。これでTinyPNGのパンダさんともしばらくお別れです。いつかサーバーサイドで動かして、Gitの二重管理から開放されたいと思います。