なぜ僕は(2015年のフロントエンドで、makeではなく)gulpを選ぶのか

http://d.hatena.ne.jp/m-hiyama/20150511/1431306678 の件

最初に

僕もgulpが今後生き残るかというと、かなり懐疑的です。開発パラダイムに合わせて変わっていくで、来年の段階で自分はgulp使えないなといっている可能性は十二分にあります。そのタイミングの一つはES6 import がHTTP2で並列ロードのオーバーヘッド無しで解決されるようになるタイミングでしょう。

根本的な問題として、Web周りは標準化の関係で動きが遅いです。最新の仕様ではままならず、ブラウザ間の実装がまちまちで、また開発上の要求が多様なのでプリプロセッサで解決する文化が根付きました。プリプロセッサがいらなくなるぐらい個々の標準が洗練されればプリプロセッサも不要になりますが、そのような未来は、今の動きをみるに、あと15年は来ないように思えます。

とはいえ、ただひとつ言えるのは、gruntはこのままだと確実に死にます。オーナーが行方不明なまま既に2年開発が止まっています。変化の速いフロントエンドでこれは致命的で、僕はgruntを避けることを推奨しています。放置されても運用に耐えうるのがgruntの設計の強みでもあるかもしれませんが、次のなにがしかのパラダイムの変化には耐えられないでしょう。

gulpの強み

で、ここから本題ですが、gulpの強みはファイル監視しつつ、差分を吐き出すのが得意なツールです。監視ベースのビルド環境では、冪等性よりもある種のマイクロマネジメントによる高速化が重要視されます。(と僕は思っています)

このファイル監視しつつ、というのがミソで、ここはmakeで表現しづらい部分です。ファイル監視下のインクリメンタルビルドではリロード時に変更が適用されていることを期待するため、できるだけ高速であることを重視されます。具体的には1s以下なら十分で、そのためのストリーム処理です。基本的な運用として、ファイル監視をぶら下げておいて、エディタで編集しつつ、ブラウザでリロード、その過程で裏で走っているビルドタスクの存在を意識しないようにするのが自分の目的です。

フロントエンド開発の一番のボトルネックはファイルIOで、コンパイル時のCPU負荷ではありません。gulpの強みは、makeのようにビルドタスクの依存の明示しつつ、オンメモリなストリームで中間状態を保持しつつ高速に差分をビルドできることです。(というのは元記事で言及されていますね)

元記事の例ではインクリメンタルビルドまで至ってないのでgulpがyet onotherなものの一つにみえているような印象を受けました。

たとえば、僕が昨日書いたgulpfile.coffeeです。

gulp   = require 'gulp'
coffee = require 'gulp-coffee'
sass   = require 'gulp-sass'
reiny  = require 'gulp-reiny'
babel  = require 'gulp-babel'
watchify = require 'gulp-watchify'

gulp.task 'default', ['build']
gulp.task 'build', [
  'build:js'
  'build:coffee'
  'build:reiny'
  'build:css'
]

gulp.task 'build:js', ->
  gulp.src('src/**/*.js')
    .pipe(babel())
    .pipe(gulp.dest('_lib'))

gulp.task 'build:coffee', ->
  gulp.src('src/**/*.coffee')
    .pipe(coffee())
    .pipe(gulp.dest('_lib'))

gulp.task 'build:reiny', ->
  gulp.src('src/**/*.reiny')
    .pipe reiny()
    .pipe(gulp.dest('_lib'))

gulp.task 'build:css', ->
  gulp
    .src('styles/style.scss')
    .pipe(sass())
    .pipe(gulp.dest('public'))

watching = false
gulp.task 'enable-watch-mode', -> watching = true
gulp.task 'browserify', watchify (watchify) ->
  gulp.src '_lib/index.js'
    .pipe watchify
      watch: watching
    .pipe gulp.dest 'public'

gulp.task 'watchify', ['enable-watch-mode', 'browserify']
gulp.task 'watch', ['build', 'enable-watch-mode', 'watchify'], ->
  gulp.watch 'src/**/*.coffee', ['build:coffee']
  gulp.watch 'src/**/*.js', ['build:js']
  gulp.watch 'src/**/*.reiny', ['build:reiny']

これのキモはwatchifyが.jsの差分監視でインクリメンタルビルドしているところですが、インクリメンタルビルドの回数を最小限にするために、watchタスクで差分だけ常に吐き出すようにしています。

常にすべてのタスクを流すと、分量によりますが、大規模な環境で10秒以上かかるのが、この環境だと何を変更しても20ms程度で終わります。その分、fdに負荷をかけているのですが、 それは現代においてあまり問題ではありません。

gulpとtypescriptについて

そもそも例が悪くて、相性が悪いです。typescriptが常にエントリポイントからの一括のコンパイルで、インクリメンタルビルドできないので。そもそもtscを使う限りはgulpのvinyl2のストリームの仕様を満たすのが困難です。

コンパイラの起動のオーバヘッドも大きいので tsc -w で運用するのが現実的だと思います。

ちなみにgulpは自身のストリームの仕様を満たさないプラグインブラックリストとして登録する場所があって、npmのgulpタグがついたリポジトリを日々精査しています。個人的に何様だという感じで、僕がgulpを気に食わない点の一つです。