npmで複数プロジェクトを運用して困ったことと、yarn・pnpm・bunに乗り換える判断基準
VPS上のNode.jsプロジェクトをnpmで運用して実際に困ったのはキャッシュ権限とnode_modulesの肥大だった。どの症状が出たらyarn・pnpm・bunを検討すべきか、npmユーザー目線の判断基準で書く。
自分はVPS上で複数のNode.jsプロジェクトを動かしている。このブログ(Astro 5、ブラウザツールやゲームも同居)、案件管理用のNext.jsダッシュボード、それに毎日cronで回るスクレイパー群。パッケージマネージャは全部npmだ。
「npm vs yarn vs pnpm vs bun」の比較記事は世の中に山ほどあるが、正直なところベンチマークの表を眺めても乗り換えの判断はできなかった。実際に効いたのは「npmで運用していて、どんな症状で困ったか」の方だ。だからこの記事は機能の網羅比較ではなく、自分がnpm運用で実際に踏んだ問題と、「この症状が出たら乗り換えを検討する価値がある」という判断基準を書く。
実際に困ったこと1: npmのキャッシュが権限で死ぬ
一番地味で、一番時間を食った問題がこれ。VPS上のあるプロジェクトでnpm installがEACCES系のエラーで落ちるようになった。原因はnpmの既定キャッシュディレクトリ(~/.npm)が、実行ユーザーの権限の都合で実質読み取り専用になっていたこと。過去にrootでnpmを叩いた形跡が残っていたりすると、こういう状態になる。
結局、その環境では毎回キャッシュの場所を明示することで回避している。
npm install --cache /tmp/npm-cache
.npmrcにcache=/tmp/npm-cacheと書いてもいいが、自分は複数ユーザー・複数プロジェクトが絡む環境なので、コマンド側で明示する運用にした。デプロイスクリプトやcronにもこのフラグを入れてある。
これはnpm固有の欠陥というより「グローバルな共有ディレクトリに書き込む設計のツール全般」が踏む問題で、pnpmのグローバルストアでも条件が揃えば同種のことは起きうる。つまり乗り換えでは解決しない。挙げたのは、比較記事でよく語られる速度やディスクより先に、実運用ではこういう泥臭い権限問題の方が先に来る、と言いたかったからだ。
実際に困ったこと2: node_modulesがディスクを圧迫する
こちらは乗り換えで構造的に解決する問題。VPSのディスクは潤沢ではないのに、du -sh */node_modulesを叩くと数百MB級のディレクトリがプロジェクトの数だけ並ぶ。AstroのプロジェクトとNext.jsのプロジェクトとスクレイパーで、esbuildだのTypeScriptだの同じ依存を何重にも物理コピーして持っている。npmはプロジェクトごとにnode_modulesへ実体を展開する方式だから、これは仕様どおりの動作だ。
pnpmはここが根本的に違う。パッケージの実体はマシン全体で1つのグローバルストアに置き、各プロジェクトのnode_modulesにはハードリンクを張る。同じバージョンのパッケージは物理的に1つしか存在しないので、プロジェクトが増えるほど差が開く。インストールが速い傾向があるのも同じ理由で、ストアに実体があればファイルのコピーではなくリンク作成で済むからだ。具体的な倍率は環境依存なのでここでは書かないが、仕組み上、2プロジェクト目以降ほど効く。
自分のVPSは「同種の依存を持つプロジェクトが複数並ぶ」というpnpmが最も効く典型ケースなので、ディスク警告が現実的になったら最初に検討するのはpnpmだと決めている。まだ移行していないのは、後述するとおりnpmのままで回るcron・デプロイの仕組みを崩したくないからで、これはこれで判断だ。
実際に困ったこと3というより、事故を防いでいる運用: lockfileは必ずコミットする
うちのプロジェクトはpackage-lock.jsonを必ずコミットして、自動化された環境ではnpm installではなくnpm ciを使う。lockfileどおりに入れて、ズレていれば失敗してくれるので、「手元では動くのにVPSで動かない」の一因を潰せる。
パッケージマネージャの話でこれに触れるのは、混在事故の温床がまさにここだから。npmはpackage-lock.json、yarnはyarn.lock、pnpmはpnpm-lock.yaml、bunはbun.lockと、ツールごとにlockfileが違う。誰かが一度yarnで入れただけでリポジトリにyarn.lockが生えて、以降どちらが正なのか分からなくなる。乗り換えるなら中途半端が一番危なくて、旧lockfileの削除とpackage.jsonへの宣言までやり切る必要がある。
{
"packageManager": "pnpm@9.0.0"
}
packageManagerフィールドを書いてCorepackを有効化(corepack enable)しておけば、違うツールで触ろうとした時点で弾かれる。逆に言うと、ここまでやる気がないなら乗り換えない方がましだと思っている。
じゃあなぜ自分はまだnpmなのか
理由は消極的だが実務的だ。
Node.jsに同梱されている。 うちのVPSではcronが毎日スクレイパーを回し、デプロイスクリプトがビルドを叩く。これらは全部「素のNode環境にnpmがある」前提で書いてある。pnpmに移行すると、この前提を全スクリプト・全環境で更新して回ることになる。動いている自動化を触るコストは、比較表には出てこないが一番重い。
困りごとの深刻度がまだ閾値を超えていない。 ディスクは圧迫されているが枯渇はしていない。インストールは遅いが、毎日のcronで待つのは人間ではなくマシンだ。症状が実害になったら移行する、でいまは足りている。
つまり自分の結論は「npmがベスト」ではなく「乗り換えの利益がまだ移行コストを上回っていない」で、これは症状次第でひっくり返る。
症状別・乗り換えを検討するライン
npmで運用している人向けに、自分ならこの症状が出たらこれを検討する、という基準を置いておく。
同種の依存を持つプロジェクトが複数あり、ディスクが現実的に苦しい → pnpm。 上に書いたとおりハードリンク+グローバルストアの構造がそのまま効く。モノレポをやるならワークスペース周りの評判も良く、この用途で名前が挙がる筆頭だ。
「package.jsonに書いていない依存をrequireできてしまっていた」系のバグを踏んだ → pnpm。 npmのnode_modulesはフラットに展開されるので、直接依存していないパッケージも偶然importできてしまう(phantom dependency)。依存側の構成変更で突然壊れる嫌なバグになる。pnpmは直接依存しか見えない厳格な構造なので、この種の事故を仕組みで防げる。
CIのインストール時間が明確なボトルネック → pnpm か bun。 どちらもnpmより速い傾向が一般に報告されている。ただしCIはキャッシュ設定の巧拙の影響が大きいので、乗り換えの前にactions/setup-nodeのキャッシュとnpm ciを正しく使えているか確認する方が先だ。自分はそれで十分な速度に収まっている。
パッケージマネージャどころかランタイムごと速くしたい → bun。ただし慎重に。 bunはランタイム・テストランナー・バンドラーまで統合していて思想としては面白いが、パッケージマネージャの乗り換えとはリスクの次元が違う。ランタイム周りの比較はDeno 2 vs Bun vs Node.jsの記事に書いた。
yarnは、今から乗り換え先として選ぶ理由を自分は見つけられていない。 yarn 1はメンテナンスモードで、yarn 2以降のPnPはnode_modulesを廃止する野心的な設計だが互換性の注意点が多い。既にyarnで回っているチームが使い続けるのは合理的でも、npmから今乗り換えるならpnpmの方が得るものが分かりやすい、というのが外から見ての意見だ(自分はyarnの長期運用経験はないので、その前提で読んでほしい)。
乗り換えないのも判断のうち
パッケージマネージャの比較は「新しくて速いものが正義」の空気になりがちだが、実際に運用してみると、速度よりも権限・lockfile・自動化スクリプトとの整合みたいな地味な部分で時間が溶ける。npmは遅くてディスクを食うが、どこにでもあって、cronの中でもCIの中でも黙って動く。その退屈さは運用では立派な機能だ。
だから自分の推奨はシンプルで、「症状が出ていないなら今のツールのまま、lockfileのコミットとnpm ciだけ徹底する。症状が出たら、上のラインに従って移行を中途半端にせずやり切る」。比較表を眺めて雰囲気で乗り換えるのが一番事故る。