gulp4 タスクのパフォーマンス改善

## はじめに

gulpタスクは開発作業中に繰り返し呼び出され続けます。そのため、gulpタスクの処理時間短縮は、開発の快適性に直結します。

gulp4をしばらく使ってみた結果、タスクの処理速度を快適に保つためにやるべきこと / やってはいけないことが見えてきました。この記事はその数値的裏付けとして実験を行い、gulpタスクを快適に保つための手法を共有するためのものです。

### 実行環境

実行環境は以下の通りです。

-   macOS 10.14.5
-   node v12.2.0
-   gulp v4.0.2

この記事の手法はgulp4以降専用です。gulp3以下には適用できないものがあります。

### この記事の想定する読者

この記事は、以下の読者を想定しています。

-   すでにgulp4を使用している。
-   gulp3を使用しているが、マイグレーションの予定がある。
-   過去gulpを使用していたが、現在は使用していない。

そのため、gulpのインストール、タスク構築などの内容は取り扱いません。

## 実験

実験を行い、どのようなgulpの呼び方をするとパフォーマンスに影響を与えるかの確認しました。
実験に使用したファイルはGitHubにアップロードしました。

[performance-gulp-and-npm-series](https://github.com/MasatoMakino/performance-gulp-and-npm-series)

実験結果はPCのスペックで大幅に変わりますので、皆様のお手元でも実行してみてください。

### 実験方法

#### 最小限のタスク

まず、実験用に最小限のgulpタスクを作りました。

```js
let count = 0;
const task = cb => {
  console.log("run" + count);
  count++;
  cb();
};
exports.task = task;
exports.default = task;
```

`task`は、カウンターの値をコンソールに出力するだけのものです。このタスク自体の実行時間は2ms ~ 400μs程度です。

#### gulpfile.jsのrequire

次に、一般的にWeb制作で利用されているnodeモジュールをgulpfile.jsにrequireで読み込みます。これらのモジュールは今回の実験用タスクでは使用しません。読み込むだけです。

```js
const webpack = require("webpack");
const sass = require("gulp-sass");
const imagemin = require("gulp-imagemin");
const plumber = require("gulp-plumber");
const browserSync = require("browser-sync").create();
const compress = require("compression");
```

このモジュール読み込む`gulpfile.js`と、読み込まない`gulpfile.simple.js`の2つのgulpfileを用意しました。

このタスクをさまざまな環境で連続実行し、どの程度パフォーマンスに差が出るかを計測しました。

### 実験結果

先に、実験結果を表にまとめておきます。`task`をそれぞれ10回実行させた場合の処理時間を比較しました。

| コマンド      | require | 並列 / 直列 | 処理時間   |
| --------- | ------- | ------- | ------ |
| npx       | 多い      | 直列      | 10.67s |
|           |         | 並列      | 6.37s  |
|           | なし      | 直列      | 5.18s  |
|           |         | 並列      | 2.35s  |
| gulp      | 多い      | 直列      | 1.37s  |
|           |         | 並列      | 1.23s  |
| gulp-cli  | 多い      | 直列      | 1.29s  |
|           |         | 並列      | 1.16s  |
| node（比較用） | なし      | 直列      | 1.14s  |
|           |         | 並列      | 0.51s  |

最後のnodeは、gulpを介さずにconsole.logで出力を行わせた場合の処理時間です。

#### npm-scripts上で実行 (直列10.67s / 並列6.37s)

まず、モジュール読み込みを行う`gulpfile.js`を、それぞれnpm-scripts上で直列 / 並列実行してみました。直列の場合、コマンドは以下のようになります。

*▼package.json*

```json
"npmSeries": "npx gulp && npx gulp && npx gulp && npx gulp && npx gulp && npx gulp && npx gulp && npx gulp && npx gulp && npx gulp"
```

この処理方法が今回の実験ではもっとも処理時間が長くなりました。

なぜ処理時間が大きくなるのかは、以下のログから推測できます。

```console
$ npx gulp && npx gulp && npx gulp && npx gulp && npx gulp && npx gulp && npx gulp && npx gulp && npx gulp && npx gulp
[14:15:04] Using gulpfile ~/Documents/performance-gulp-and-npm-series/gulpfile.js
[14:15:04] Starting 'default'...
run0
[14:15:04] Finished 'default' after 2.67 ms
[14:15:05] Using gulpfile ~/Documents/performance-gulp-and-npm-series/gulpfile.js
[14:15:05] Starting 'default'...
run0
[14:15:05] Finished 'default' after 2.08 ms
[14:15:06] Using gulpfile ~/Documents/performance-gulp-and-npm-series/gulpfile.js
[14:15:06] Starting 'default'...
run0
[14:15:06] Finished 'default' after 1.99 ms
...
```

カウンターが加算されず、常に`run0`と出力されています。これは`npx gulp`コマンドごとに、gulpfileの再読み込みが行われているためです。該当する処理は[こちら](https://github.com/gulpjs/gulp-cli/blob/master/lib/versioned/%5E4.0.0/index.js#L35)です。この再読み込みに時間がかかります。

#### npm-scripts上で`gulpfile.simple.js`を実行 (直列5.18s / 並列2.35s)

次に、モジュールの読み込まない`gulpfile.simple.js`を使って同様の処理を行いました。直列の場合、コマンドは以下のようになります。

*▼package.json*

```json
"npmSeriesSimple": "npx gulp -f gulpfile.simple.js && npx gulp -f gulpfile.simple.js && npx gulp -f gulpfile.simple.js && npx gulp -f gulpfile.simple.js && npx gulp -f gulpfile.simple.js && npx gulp -f gulpfile.simple.js && npx gulp -f gulpfile.simple.js && npx gulp -f gulpfile.simple.js && npx gulp -f gulpfile.simple.js && npx gulp -f gulpfile.simple.js"
```

[gulp-cli](https://www.npmjs.com/package/gulp-cli)はgulpに同梱されているCLIモジュールです。コマンドに[引数](https://www.npmjs.com/package/gulp-cli#flags)を与えることでgulpコマンドの挙動を操作できます。--gulpfile(-f)フラグを指定することで、任意のファイルをgulpfile.jsの代わりに読み込ませることができます。今回はこのフラグを利用して`gulpfile.simple.js`を読み込ませました。

結果はモジュール読み込みありと比べ、処理時間が短くなりました。タスクで使用しないモジュールでも、gulpfile内で読み込みを行うと全体の処理時間が伸びます。

#### `gulp.series` / `gulp.parallel` で実行 (直列1.37s / 並列1.23s)

gulpfile.js内で`gulp.series` / `gulp.parallel`を利用し、複数のタスクを1つのタスクにまとめてから実行します。直列の場合は以下のようになります。

*▼gulptask.js*

```js
exports.seriesTask = series(
  task, task, task, task, task, task, task, task, task, task
);
```

処理時間は、npm-scriptsで並列 / 直列実行するよりも大幅に短くなります。ログを見てみると

```console
$ npx gulp seriesTask
[14:54:37] Using gulpfile ~/Documents/performance-gulp-and-npm-series/gulpfile.js
[14:54:37] Starting 'seriesTask'...
[14:54:37] Starting 'task'...
run0
[14:54:37] Finished 'task' after 1.02 ms
[14:54:37] Starting 'task'...
run1
[14:54:37] Finished 'task' after 314 μs
[14:54:37] Starting 'task'...
run2
[14:54:37] Finished 'task' after 216 μs
...(中略)...
[14:54:37] Starting 'task'...
run9
[14:54:37] Finished 'task' after 188 μs
```

のようにカウンターが加算されているのがわかります。gulpfileの再読み込みがないため、トータルの処理時間も短くなっています。

#### gulp-cliで直列 / 並列処理 (直列1.29s / 並列1.16s)

[gulp-cli](https://www.npmjs.com/package/gulp-cli)の`--series`フラグを利用すると、タスクをnpm-scripts上で直列処理できます。タスク名を実行順に並べ、最後に`--series`フラグをつければ直列処理になります。

*▼package.json*

```json
"gulpSeriesCLI": "npx gulp task task task task task task task task task task --series"
```

`--series`フラグをつけずに実行すると並列処理になります。
パフォーマンスはgulpfile.js内で`gulp.series` / `gulp.parallel`を利用した場合と同等になります。今回の結果では`gulp.series` / `gulp.parallel`よりもわずかに早い結果が出ていますが、計測誤差の範囲内と思われます。

## まとめ

実験結果から、gulpタスクを効率よく処理するための方針が把握できました。

### gulpコマンドは重い

`gulp`コマンドはgulpfile.jsとそこから呼び出されるモジュール類を読み込む処理です。この処理には時間がかかります。gulpコマンドの回数を減らした分だけ処理時間は短くなります。

### モジュールの読み込みは重い

`require`した分だけ、タスクの処理時間は伸びていきます。未使用のモジュールがあるなら、gulpfileから削除しましょう。

### gulpコマンドの回数を減らすために、gulp.series / gulp.parallelを積極的に利用する

`gulp`コマンドの回数を減らすには、series(), parallel()関数が効果的です。

### CLIでもseries / parallelが可能

gulp-cliでの直列、並列処理はseries(), parallel()関数と同じ効果があります。

### どうしてもgulpコマンドを分割する必要があるなら、gulpfileも分割することを検討する。

gulpfile.jsで読み込むモジュールの数が減ると、gulpコマンドの処理時間は短くなります。

たとえば、gulpタスクA → npm-scripts → gulpタスクBという順番での処理がどうしても必要になった場合は、タスクAとタスクBのgulpfileを分割することを検討してください。
この例で言えば`gulpfile.js`と`gulpfile.b.js`を作成し、gulpタスクBでは`-f gulpfile.b.js`フラグをつけて実行します。`gulpfile.b.js`では処理に必要なモジュールのみを読み込むことで、全体の処理時間を短くできます。

以上、ありがとうございました。