npm

npm ciを使おう あるいはより速く

人類はより高速にCIを回していくべきだと思っている りんご(@mstssk)です。

先日、 npm の v5.7がリリースされ npm ci というサブコマンドが新たに追加されました。

The npm Blog — Introducing npm ci for faster, more reliable... http://blog.npmjs.org/post/171556855892/introducing-npm-ci-for-faster-more-reliable

CI/CDを開発プロセスに組み込んでいる場合により整合性があり高速なエクスペリエンスを提供する、と公式ブログでは紹介しています。

npm ci は何をするのか

npm ci を実行すると常に package-lock.json から依存関係をインストールします。
既に node_modules フォルダの中身があっても一旦削除します。

従来の npm install コマンドを実行すると、 package.json と package-lock.json の両方を見て依存関係の解決と依存パッケージの node_modules へのインストールを行います。 package.json を解決して必要に応じてロックファイルである package-lock.json の更新もします。

一方で npm ci は package.json の依存関係の解決を行わず、常に package-lock.json を見て依存パッケージをダウンロードし node_modules の洗い替えを行います。
しかし、 package.json を完全に無視するというわけではなく、 package-lock.json と依存バージョン指定が食い違っているとエラーにしてくれます。例えば、あるパッケージを v0.8.9でインストールし package-lock.json まで作成済みの時に、そのパッケージを更新しようとして package.json だけ v0.9.0 に書き換えてしまった状態で npm ci を実行するとエラーになります。

このように、依存関係の更新をせずに整合性チェックと依存パッケージのダウンロードのみを行うため npm install より高速に動作し、CIで必要なことだけを行うのが npm ci コマンドです。

npm ci は銀の弾丸ではない

npm ci が package.json と package-lock.json の整合性をチェックしてくれるのは嬉しいです。
一方で、依存パッケージのダウンロード・インストールの高速化については、爆速になるというほどではありません。

私が普段触っているWebサイト構築プロジェクトいくつかで時間を計測してみました。

npm install npm ci
プロジェクトA 25.515s 16.656s
プロジェクトB 29.761s 20.676s
プロジェクトC 54.380s 40.710s

※いずれも node_modules が存在しない状態で計測。

どの場合でも早くなるのは10秒前後でした。この10秒が package.json を見て依存関係の解決を行う時間なのでしょう。

削減できなかった時間が何なのかというと、ログを見ている限りでは各依存パッケージの preinstall / postinstall のスクリプトの実行時間のようです。
例えば node-sass パッケージは実行環境ごとのバイナリのインストールや動作チェックを行っているようですし、その他にもインストール時に node-gyp を使ってネイティブモジュールをビルドしている、なんてものもありました。

思い切って node_modules をキャッシュすると早い

CIの速度を重視するなら npm ci を使うのではなく、もっと愚直に node_modules をキャッシュしてしまうという手段もあります。

例えば、私は普段CircleCIの config.yml で次のように設定して、node_modules をキャッシュさせています。

.circleci/config.yml
version: 2
jobs:
  build:
    steps:
      - checkout
      - restore_cache:
          keys:
            - npm-cache-{{ checksum "package-lock.json" }}
      - run: npm install
      - save_cache:
          key: npm-cache-{{ checksum "package-lock.json" }}
          paths:
            - ./node_modules
      /* 後略 */

この方法によって、通常 npm install に54秒かかっていたプロジェクトも、キャッシュのレストアと npm install の時間あわせて15秒程度で済んでいます。
もちろん、最初の1回や package-lock.json を更新した際は時間がかかってしまいますが、多くの場合で大幅に速度改善になります。

ただし、この方法では npm ci が提供する package.json と package-lock.json の整合性チェックが使えません。
npm ci は node_modules があると一旦消して洗い替えしてしまうのでキャッシュと一緒に使用できません。
CIで何を担保するのかを考慮した上でキャッシュの使用を検討するとよいでしょう。

まとめ

npm ci は既にCIでnpmを使っているプロジェクトへの強力なサポートとなります。
どうしても速度を改善したい場合は node_modules をキャッシュしてしまう方法もあります。

みなさんもプロジェクトにあわせてCIをメンテしていきましょう。