なぜ 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