なぜ pnpm が速いのかを調べてみた

最近「高速」、「ディスク容量効率が良い」と謳ってる pnpm がよく耳に入りますね。
職場でも本気の移行を検証しています(まだ POC 段階だけど)。
どうやって「高速」、「ディスク容量効率が良い」を実現できるのを調査してみました。
先に npm や yarn はどんな課題があるのかを考えてみよう。
npm2
まずはnpm2から見てみよう。
(Node.jsバージョンを4にしたら、npmが2.xになります。)
$ node -v
v4.0.0
$ npm -v
2.14.2
# 初期化して、express をインストールします
$ npm init -y
$ npm install express
インストールしたら express も依存パッケージもダウンロードしてくれました。
node_modules/expressの下にさらにnode_modulesがあります。

さらに展開してみると、各依存パッケージも自分の node_modules を持っています。

つまり npm2 の node_modules がネスト構造になっています。
うん?別に良いじゃない?なにか問題でもありますか?
あります。
npm2 の課題
各パッケージには同じ依存パッケージを持つかもしれないので、ネスト構造になると同じ依存パッケージが何回もコピーされるので、ディスク容量がいっぱい消費されます。
それだけじゃなく、Windows でサポートされているパス長がデフォルトで 260 文字までという制限があります(解除方法もありますけど)。このようなネスト構造になると簡単に制限を超えます。
npm 2.x が解決できなかった課題について、コミュニティが答えを出しました。
それがyarnです。
yarn
yarn はどうやって「依存パッケージの重複」や「パス長すぎる」の課題を解決したのか?
その答えはフラット化です。すべての依存パッケージを同じ階層に置いてたら、重複の依存パッケージも長すぎるパスもなくなりました。
node_modules を削除して、yarn でもう一回インストールしてみます。
$ yarn add express
node_modulesはこのようになりました:

すべての依存パッケージが同じ階層になって、だいたいの依存パッケージの下にも node_modules がなくなりました。
例外にいつかのパッケージの下にまだ node_modules が存在しています:

なぜならば、同じパッケージでもいくつかバージョンがあります。バージョンの一つだけが node_modules の直下に置かれて、他のバージョンはこれまでと同じくネスト構造になります。
npmバージョン 3 からも yarn と同じ感じのフラット化になりました。
他に yarn.lock を利用してバージョンを固定する機能もありますが、そのあと npm も同じ機能を実装しました(package-lock.json)。
フラット化になったらで良いのか? デメリットがないのか?
And what, Gul'dan,must we give in return?
あります。
yarn/npm3+ の課題
それが Phantom dependencies (幽霊的な依存)の問題です。つまり、dependencies に書いてない依存でも、コードで require / import できます。同じ階層になっていますので当然できます。
何が問題というと、明示的に依頼してないので、もしほかのパッケージがこのパッケージを依頼しなくなったらコードが動けなくなります。(dependencies に書かれてないのでインストールされなくなったので。)
もう一つの課題は、バージョンの一つだけが node_modules の直下に置かれてるので、他のバージョンはこれまでと同じく何回もコピーされ、ディスク容量が消耗されます。
もっと良い方法ありますかな?
それが今日の主役、pnpm です。
pnpm
フラット化の原因を思い出してみよう。
根本的な原因はパッケージを何回もコピーしたのです。
ではコピーをやめたらどうでしょう? 例えば link を利用します。
Wikipedia 上の link の説明
ファイルシステム上のファイルやディレクトリのデータとその名前を結びつけるリンクがあり、ハードリンクとシンボリックリンクの2種類があります。
- ハードリンクは、コンピュータのファイルシステム上のファイルやディレクトリ等の資源とその資源につけられた名前を結びつけること、もしくは、その結びつきのことである。
- シンブルリンク(ソフトリンク)は、コンピュータのディスク上で扱うファイルやディレクトリを、本来の位置にファイルを残しつつそれとは別の場所に置いたり別名を付けてアクセスする手段である。複製とは違い、実体がないこと、ソフトリンクで開いたファイルへの操作が実物のファイルにも反映されること、ファイルサイズが小さいのが特徴。
コピーせずにディスク上の 1 つの場所にパッケージを保存して、そのほかは link で結べたら?
ディスクの浪費もないし、長すぎるパス問題も解決できます。
それが pnpm です。
では node_modules を削除して、pnpm でもう一度インストールしましょう。
$ pnpm install
Packages: +57
+++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Packages are hard linked from the content-addressable store to the virtual store.
Content-addressable store is at: /Users/yshi/Library/pnpm/store/v3
Virtual store is at: node_modules/.pnpm
この行を見ると:
Packages are hard linked from the content-addressable store to the virtual store.
パッケージのファイル(content-addressable store)はバーチャルストアからハードリンクされます。バーチャルストアは node_modules/.pnpm です。
確かに node_modules の下は express しか存在しない、Phantom dependenciesが存在しません。

.pnpm を展開してみるとこんな感じになります:

すべての依頼がフラットになって(グローバルストアとのハードリンク)、パッケージ間はシンブルリンクで繋がってます。
公式の説明図もわかりやすいです:

これが pnpm の原理です。
link を活用してパッケージを一カ所にしか保存しないため、ディスクを消費を抑えますし、スピードも当然上がります。
それで「高速」、「ディスク容量効率が良い」を実現しました。
(Mavenと似てます。)
npm2やyarn/npm3+の課題を解決し、モノレポもサポートしてるし、かなり優秀ですね。これからどんどんはやると思いますね。
デメリットは?
And what, Gul'dan,must we give in return?
グローバルストアの掃除
パッケージはグローバルストアに保存されてるから、依頼パッケージを削除してもグローバルストアにそのまま残されます。
グローバルストアの掃除は自分でやらないいけないのが少し面倒なところです。(気にしないなら掃除しなくてもよいけど)
$ pnpm store prune
シンブルリンクをサポートしない環境では使えない
electron などシンブルリンクをサポートしない環境では使えない
参考
Discussion