npm, yarn による zero install 戦略

jsconf.eu 2019 に行ってきました。 特に npm や yarn の今後の話とそもそも Registry をどうしていくか、の話があったのでお知らせします。 そもそも Registry をどうしていくかについては次のエントリで話します。

tink: A Next Generation Package Manager

npm の次のコマンドラインツールである tink が紹介されていました。

github.com

presentation: github.com video:

www.youtube.com

そもそも npm の仕組み

  1. ローカル依存ファイルを読む (package.json, package-lock.json, shrinkwrap.json)
  2. 存在しないパッケージのメタデータをfetchする
  3. 木構造を計算して、実行する(npm v3 以降だとflattenする)
  4. 実際に存在しないパッケージをダウンロードする
  5. インストールスクリプトを実行する

今のどこがダメなのか

npm は tar ball を fetch して、そこから gunzip して、最後にできあがったファイルをnode_modules以下にコピーするという非常にピーキーな処理をしています。最初のfetchは network IO に影響し、 gunzipCPU に影響し、ファイルコピーは file IO に影響します。

これを簡単にするためにキャッシュしたり、展開済みのファイルをハードリンクさせたりとチューニングをしています。 これ自身は必要な処理なのですが、実体のファイルとして作る関係上、どうしても node_modules は巨大になります。

https://cdn-ak.f.st-hatena.com/images/fotolife/y/yosuke_furukawa/20180604/20180604215229.png

tink sh

tinknode 本体の代替として起動します。

$ node foo.js

で起動するのではなく、

$ tink sh foo.js

で起動します。これで何がやりたいかというと、 node_modules を物理的なフォルダにするのではなく、仮想上のフォルダにすることを目指してます。

仮想上のフォルダになることのメリット

いくつか利点がありますが、まず実際にファイルをコピーする必要がない分、 File IO への影響は緩和されます。

また、 tink 自身はローカル内にハッシュ値を基にしたキャッシュを作るので、ハッシュ値が一致したパッケージに関してはキャッシュが使われます。  Network IO への影響も緩和されます。

また、最大のメリットとして、 tink sh で実行した際にランタイムで依存モジュールを解決するため、 npm install のコマンド実行が不要になります

つまり、 git clonegit pull してから依存ファイルがなかったとしても tink sh <cmd> で起動すれば実行時に依存モジュールを解決し、起動することができます。

virtual node_modules
virtual node_modules

この試みのことを zero install と呼びます。

prepare && unwind

本番環境で Docker 等を使って毎回ゼロからイメージを作ってる場合はイメージ作成後のキャッシュがないため、毎回起動時にパッケージのダウンロードが開始されます。

これを解決するために事前にFetchしてくるコマンド(prepare)と事前に node_modules を作るコマンド(unwind)の2つが提供されています。

$ tink prepare # 事前にfetchしてくる
$ tink unwind # node_modulesの実体を作る

load map

tinknpm v8npm コマンドに統合される予定とのことでした。

f:id:yosuke_furukawa:20190610102508p:plain

yarn v2: berry

presentation:

www.youtube.com

yarn v2tink と同様の戦略です。zero install を実現しています。 .pnp.js というファイルを内部に作成し、そこに依存モジュールを解決できるようにしています。

開発ツールとしては tinkyarn v2 も同じ戦略を取っていました。細かな機能の違いはあるのでいくつか紹介します。

yarn contraints

package.json と実行時の依存関係に矛盾があった場合に、警告した上でpackage.jsonを修正してくれるサブコマンドです。

一番多いユースケースは、実行時に依存してる package が package.json に書かれてなかったとか、 workspace と一緒に使った際にmonorepoのサブパッケージ間で異なるバージョンのライブラリに依存してた等ですね。

いわば、 package.json の linter みたいなものですね。

どうやって実現しているのか

tinkyarn v2 も実態は node のプロセスである以上、 node 側の標準モジュールでは node_modules 以下にあるファイルをロードしに行く必要があります。 これを解決するために、 tinkyarn v2 では node の標準APImodule にパッチを当てています(!)

module のファイルロードを行う箇所のメソッドを拡張し、 node_modules がなくてもファイルをロードできるようにしています。

標準のモジュールロードからは逸脱しているため、今のところ使うのは危険ではあります。将来的に読み込み方が変わる可能性もありますし、標準の node の読み込み方を変更する可能性もあるので、まだ experimental と言ったところでしょうか。

ただし、 electron も同様の手法でモジュールを読み込めるように拡張しているので、そこまで変更がドラスティックに加えにくい箇所でもありますね。

実際に、 tink の解説では、 electron も同じことやってる(ので大丈夫)というニュアンスで発表していました。

f:id:yosuke_furukawa:20190610101630p:plain

まとめ

モジュール管理は zero install 時代になっていくと思います。事前の処理を不要にすることで開発効率を上げて、 git clonegit pull から何もしなくても起動できるようになっていくと思われます。ただ一方で実際に本番で zero install にしてしまうと、起動してから実際に動作するまでのパフォーマンスは落ちる可能性もあるため、起動方法は変更になるかと思われます。

これらを解決するための本番環境用のコマンドとして、 tink preparetink unwind も公開されているので、それらを使って行くのではないかと思っています。

また一方で、 tinkyarn v2 もほとんど同じことをしているので、どちらにも有意差があるようには見えず、状況は変わらないまま今後に突入していくのではないかと思われます。

これらを踏まえて、 Registry についての話を書きます。また次回!