Node.jsのパッケージ管理は、本当に健全なのか?pnpmを調べていく中で、依存管理の“当たり前”を見直すことになりました。この記事ではpnpmの設計思想に焦点を当てて整理します。
1. はじめに ― パッケージ管理は本当に健全か?
あるシステムのリプレイス検証の中で、生成AIを活用しながら新規構築を進めていました。既存案件ではnpmやYarnを使ってきましたが、今回は検証フェーズということもあり、せっかくならモダンな構成を試してみようと考えていました。
セットアップを進める中で、生成AIから自然と提案されたのが pnpm(Performant npm) です。これまで特に困っていたわけではありませんが、新しい選択肢に触れる良い機会だと思い、採用してみることにしました。
使い始めてみると、単に「速い」というだけではなく、依存関係の扱い方そのものに明確な思想があることに気づきます。なぜこれまで宣言していない依存にアクセスできていたのか。なぜnode_modulesはあれほど複雑なのか。
この機会にパッケージ管理の仕組みを改めて調べてみたので、その内容を整理します。
2. node_modulesの歴史と“歪み”
フラット化されていった依存構造
Node.js初期のパッケージ管理方法では、依存パッケージはそれぞれのnode_modules配下にネストして配置されていました。
node_modules/
package-a/
node_modules/
package-b/
この構造は理論上は正しく、「宣言した依存だけが使える」状態を保てます。しかし依存の深さが増えると、ディレクトリ階層が極端に深くなり、Windowsのパス長制限やインストール速度の問題が発生しました。
そこでnpm v3以降では、依存関係を可能な限りルート直下に “フラット化” する(ホーイスティングと呼ばれる)仕組みが導入されます。
node_modules/
package-a/
package-b/
ファントム依存という構造的問題
フラット化の結果、あるパッケージが「自分で宣言していない依存」にアクセスできてしまう状況が発生しました。
本来であれば、
- package-aがpackage-bを使うなら
- package-aの
package.jsonに明示的に書くべき
ですが、たまたま別のパッケージ経由でインストールされていれば、直接参照できてしまいます。
これがいわゆる phantom dependency(幽霊依存) です。
ローカルでは動くのに、環境が変わると壊れる。
依存を削除したら別の箇所が突然エラーになる。
こうした現象は、構造上 “起き得る” 設計になっていることが原因です。
「動く」ことを優先した結果
npmやYarnの設計は、巨大なエコシステムを壊さず、できるだけ多くのプロジェクトを動かし続けることを優先してきました。
- 多少曖昧な依存宣言でも動く
- 既存資産との互換性を保つ
- インストールを高速化する
その選択自体は合理的です。しかしその裏側で、依存関係の透明性や厳密さはやや犠牲になってきました。
-
node_modulesの肥大化 - 見通しの悪さ
- 暗黙的な依存関係
これらは偶然の産物ではなく、歴史的な経緯の中で生まれた “設計上の歪み” とも言えます。
pnpmは、この前提に別のアプローチを取っています。
3. pnpmの設計思想
3-1. 依存は宣言したものだけ使えるべき
pnpmの根底にあるのは、「package.jsonに宣言した依存だけが使えるべき」というシンプルな原則です。
npm / Yarnのフラットなnode_modules構造では、別のパッケージ経由でインストールされた依存に、偶然アクセスできてしまうことがあります。(phantom dependency)
この状態では、次の不安定さが生まれます。
- 直接依存に書いていないのに動く
- 依存の解決順や更新で突然壊れる
- 環境差で再現しない不具合が出る
pnpmは非フラット構造を採用することで、原則として直接依存していないパッケージにはアクセスできないようにしています。
そのため、エラーが出る場合は「ツールが厳しい」のではなく、依存宣言の曖昧さが可視化されているだけです。
“壊れる” ことで依存が明確になる。
それが pnpmの目指す健全な依存管理です。
3-2. 同じものを何度も置くのは無駄
もう一つの大きな思想は、「同じパッケージを何度も物理的に配置するのは無駄ではないか」という点です。
npm / Yarnでは、プロジェクトごとにnode_modulesを持ちます。
複数のプロジェクトで同じバージョンのパッケージを使っていても、それぞれのディレクトリに実体が展開されます。
pnpmはここに別のアプローチを取ります。
まず、すべてのパッケージは content-addressable store(CAS) と呼ばれるグローバルストアに一度だけ保存されます。
その上で、各プロジェクトのnode_modulesには次のような二段構成でリンクが作られます。
プロジェクト
└─ node_modules/
├─ package-a -> .pnpm/package-a@1.0.0/node_modules/package-a (symlink)
└─ .pnpm/
└─ package-a@1.0.0/
└─ node_modules/
└─ package-a (hard link → グローバルストア)
グローバルストア
└─ store/
└─ sha512-xxxx/
└─ package-a の実体
つまり、論理的には非フラット構造を保ち、物理的な実体は一箇所に集約する、という設計です。
その結果、次のようなメリットが生まれます。
- ディスク使用量が抑えられる
- インストールが高速になる
- キャッシュ効率が高まる
重要なのは、非フラット構造でありながらパフォーマンスを落とさない点です。
「正しい依存構造」と「効率的なストレージ管理」を両立させるために、グローバルストア + ハードリンク + シンボリックリンクという組み合わせが採用されています。
単なる高速化テクニックではなく、
「同じものは一つでよい」という合理性を、構造レベルで実現した結果がpnpmのアーキテクチャです。
3-3. npm互換である理由
pnpmは独自の思想を持ちながらも、package.jsonやスクリプトの扱い、基本的なコマンド体系はnpmと互換性を保っています。
これは偶然ではなく、意図的な設計です。
Node.jsのエコシステムは巨大で、ほとんどのツールやライブラリはnpmを前提に作られています。
もし互換性を捨ててしまえば、既存資産との断絶が起き、導入コストは一気に跳ね上がります。
pnpmは革命を起こすのではなく、土台はそのままに内部構造を改善するアプローチを選びました。
-
package.jsonはそのまま使える - npm scriptsもそのまま動く
- 既存プロジェクトにも段階的に導入できる
思想は異なっても、エコシステムとは対立しない。
pnpmは「全部作り直す」ツールではなく、既存の仕組みを前提にしながら健全化を目指す、現実的な選択をしていると言えます。
4. なぜpnpmは“壊れる”のか
pnpmを既存プロジェクトに導入すると、「今まで動いていたのにエラーになる」という場面に遭遇することがあります。
多くの場合、その原因はツールの不具合ではありません。
これまで暗黙的に許容されていた依存関係が、明示的でないと動かなくなっただけです。
例えば、次のようなケースが該当します。
- 直接依存に書いていないパッケージを
importしていた -
peerDependenciesの扱いが曖昧だった - 依存の解決順に偶然支えられていた
npm / Yarnではたまたま動いていたこれらの状態が、pnpmではエラーとして表面化します。
pnpmは非フラット構造を採用しているため、宣言していない依存には基本的にアクセスできません。
その結果、これまで見過ごされてきた曖昧な依存が可視化されます。
つまり、pnpmが“壊す”のはプロジェクトではなく、依存宣言の曖昧さです。
“壊れる” のは、厳しいからではありません。
依存関係を明示的にしようとする設計が意図した結果なのです。
現実的な回避策:脱出ハッチの存在
とはいえ、レガシーなライブラリの都合でファントム依存を許容せざるを得ない「現場の事情」もあります。pnpmはそうした現実に対し、意図的にホーイスティングを許可する設定を用意しています。
.npmrcに以下を記述するか、インストール時に--shamefully-hoistフラグを指定します。
# すべてをルート直下に引き上げる設定
public-hoist-pattern[]=*
--shamefully-hoist(恥ずべきホーイスティング)という直球なネーミングには、「推奨はしないが、背に腹は代えられない時は使ってね」という開発者の現実的な配慮が伺えます。
「原則は厳格に、運用は柔軟に」。この脱出ハッチがあるからこそ、既存プロジェクトでも安心して導入を検討できるのです。
5. チーム開発における意味
では、その厳格さはチーム開発において何をもたらすのでしょうか。
pnpmを導入すると、まず変わるのは依存の扱い方です。
暗黙に使えていたパッケージは、すべて明示的に package.json に追加する必要があります。
その結果、次のような変化が起きます。
- どのパッケージがどこで使われているのかが明確になる
- レビュー時に依存追加の意図が見える
- 不要な依存を削除しやすくなる
依存が透明になることで、責任の所在も明確になります。
「なぜこのライブラリが入っているのか分からない」という状態が減っていきます。
また、グローバルストアによるインストール高速化は、CI時間の短縮やローカルセットアップの効率化にも寄与します。
特にモノレポ構成では、依存共有のメリットがより分かりやすくなります。
事実、昨今のトレンドであるモノレポ(Workspaces)において、「依存の効率的な共有」と「プロジェクト間の厳格な分離」を両立できる点こそが、pnpmを選択する最大の決定打となっているケースも少なくありません。
pnpmがもたらすのは単なる速度向上ではなく、依存関係が説明可能な状態になることです。
「動く環境」を共有するのではなく、
「定義された依存関係」を共有する。
それがチーム開発におけるpnpmの意味と言えます。
6. 適材適所の判断基準
pnpmは依存管理を健全にしやすい一方で、「常に最適解」というわけではありません。状況によってはnpmを選ぶのが合理的なケースもあります。
小規模・単発の用途
ちょっとした検証スクリプトや小さなツールなど、依存関係が薄く短命なプロジェクトでは、npmの手軽さが勝ちやすいです。導入コストをかけずにすぐ動かせるのが強みです。
互換性リスクを極力避けたい場合
pnpmの厳格さによって、依存宣言の曖昧さがエラーとして表面化します。既存資産が大きく、修正コストが高いプロジェクトでは、その “健全化” が短期的な負担になることがあります。
「今は安定稼働が最優先」という局面ではnpmを継続する判断もあり得ます。
チームや周辺ツールの前提がnpm中心の場合
CI、社内テンプレ、ドキュメント、開発フローがnpm前提で固まっている場合、pnpmへ移行するだけで運用コストが増えることがあります。特に、開発者体験よりも標準化を重視する組織では、npmを揃える方がスムーズです。
結局のところ、ツール選定は「優れているか」ではなく「状況に合うか」です。
pnpmは “健全性” に寄せた選択肢、npmは “手軽さと互換性” に寄せた選択肢、と捉えると判断しやすくなります。
7. まとめ ― pnpmは速度の話ではない
pnpmについて調べていく中で感じたのは、「速い」「軽い」といった分かりやすいメリットは、あくまで結果にすぎないということでした。
本質は、依存関係をどう扱うかという設計思想にあります。
- 宣言した依存だけを使う
- 同じものを何度も置かない
- 既存エコシステムと断絶しない
どれも派手ではありませんが、パッケージ管理をより透明で再現性の高いものにしようとする姿勢が一貫しています。
これまでnpmやYarnを当たり前に使ってきましたが、pnpmに触れたことで、「動くこと」と「健全であること」は必ずしも同じではないと気づかされました。
ツール選定は、単なる機能比較ではなく、どんな前提を受け入れるかという選択でもあります。
pnpmは速度の話ではなく、依存管理のあり方を問い直すツールだと思います。
KIYOラーニング株式会社について
当社のビジョンは『世界一「学びやすく、分かりやすく、続けやすい」学習手段を提供する』ことです。革新的な教育サービスを作り成長させていく事で、オンライン教育分野でナンバーワンの存在となり、世界に展開していくことを目指しています。
プロダクト
- スタディング:「学びやすく・わかりやすく・続けやすい」オンライン資格対策講座
- スタディングキャリア:資格取得者の仕事探しやキャリア形成を支援する転職サービス
- AirCourse:受け放題の動画研修がついたeラーニングシステム(LMS)
KIYOラーニング株式会社では一緒に働く仲間を募集しています