【テクニカル・上級編】巨大すぎるnode_modulesにさようなら:pnpmの「store」を共有して複数プロジェクトでディスク容量を極限まで節約する – ビルド・パッケージ管理ツール生産性向上バイブル

`node_modules`の呪縛を解く:pnpmのコンテンツアドレス可能ストレージがもたらす開発体験の真実

開発者のPCを蝕む「数GBの`node_modules`」という現代の病理。npmやYarn(v1)が引き起こしてきた、プロジェクトごとにパッケージを物理複製する非効率な設計は、もはや過去の遺物です。

本稿では、単なる「pnpmのインストール手順」を語るつもりはありません。コンテンツアドレス可能ストレージ(Content-addressable storage)というアーキテクチャの本質を突き、CI/CDパイプラインからDocker環境まで、「物理ストレージを意識させない」レベルまで最適化を突き詰めるエキスパートのための知見を共有します。

—

1. 内部構造のパラダイムシフト:なぜpnpmは速く、そして小さいのか

npmやYarnの最大の問題は、OSのファイルシステムに対し、冗長なI/Oを強いることにあります。pnpmは、パッケージを「プロジェクトごと」にコピーするのではなく、単一のグローバルストア(通常 `~/.pnpm-store`)に保存し、そこからハードリンクを用いて各プロジェクトへ展開します。

アーキテクチャの核心

  • コンテンツアドレス可能ストレージ: パッケージのソースコードのハッシュ値がファイル名となります。同じバージョンのライブラリは、マシン上に物理的にただ一つしか存在しません。
  • ハードリンクによるマッピング: `node_modules` 内の実体は、グローバルストアへのハードリンクです。これにより、ディスク容量を消費するのは「実質的な差分のみ」となります。
  • 非フラットな `node_modules`: npmのフラット構造(幽霊依存問題の温床)を捨て、シンボリックリンクを用いた階層的な構造を採用することで、依存関係の厳格な分離を実現しています。

—

2. Docker環境での「ストア共有」の極意

CI環境で毎回 `pnpm install` を実行し、Dockerレイヤーキャッシュに頼るのは効率が悪すぎます。ホスト側またはビルドキャッシュサーバーとストアを共有することで、ビルド時間を劇的に短縮します。

Dockerfileでの最適化設定

最終イメージには不要なキャッシュを排除しつつ、ストアをマウントする
FROM node:20-slim AS base
ENV PNPM_HOME=”/pnpm”
ENV PATH=”$PNPM_HOME:$PATH”
RUN corepack enable

ビルドステージでは、ストアをキャッシュとしてマウントし、高速化を図る
FROM base AS build
COPY . /app
WORKDIR /app
–mount=type=cache を使い、pnpmのストアを永続化する
RUN –mount=type=cache,id=pnpm-store,target=/pnpm/store \
pnpm install –frozen-lockfile
RUN pnpm run build

なぜこれが必要か?: `type=cache` を使うことで、Dockerビルドコンテキストを汚さず、かつコンテナ起動のたびに全パッケージを再ダウンロードする無駄を排除します。CI/CDパイプライン(GitHub Actionsの `actions/cache` など)とこのマウントポイントを連携させれば、依存関係解決の時間はほぼゼロになります。

—

3. 大規模モノレポでのメモリ消費を抑える最適化ハック

pnpmは非常に高効率ですが、数千のパッケージを抱えるモノレポでは、Node.jsのメモリ制限に抵触することがあります。ここで真価を発揮するのが `.npmrc` によるチューニングです。

`.npmrc` による極限設定

ストアの場所を明示的に指定(CI/CD環境ではパスを固定する)
store-dir = /opt/pnpm-store

ハードリンクが難しい環境(Dockerの異なるマウントポイント間など)の場合の挙動
‘clone’ を指定すると、コピーオンライトをサポートするFSで効率化される
package-import-method = auto

同時実行数を制限し、メモリスパイクを防ぐ
大規模環境では CPUコア数 2 程度に抑えるのが無難
fetch-concurrency = 4

—

4. 自動化スクリプト:ストアの健全性を守る

ストアを共有化すると、必然的に「不要なパッケージ」が蓄積されます。伝説的なDevOpsエンジニアは、ストレージのガーベッジコレクション(GC)を自動化します。

ストアの自動クリーンアップスクリプト(`prune-pnpm.sh`)

!/bin/bash
グローバルストアから孤立したパッケージを削除し、ディスクを解放する
定期的に cron や CI のクリーンアップジョブで実行する

echo “Starting pnpm store pruning…”
–prune: ストアから参照されていないパッケージを削除
–store-dir: ストアの場所を明示
pnpm store prune –store-dir /opt/pnpm-store

echo “Current store size:”
du -sh /opt/pnpm-store

—

5. アーキテクトの視点:導入の真のメリット

この設計を導入すると、以下の「目に見えない利益」が現場にもたらされます。

1. コンテキストスイッチの高速化: プロジェクトAからBへ切り替える際、`pnpm` は既にストア内に依存先を持っているため、ネットワークI/Oを一切発生させません。
2. セキュリティの向上: 依存関係が厳格に管理されるため、`node_modules` 内に隠れた「意図しない依存ライブラリ」が混入しにくくなります。これにより、脆弱性スキャン(Snykやnpm audit)の精度が向上します。
3. CI/CDパイプラインの決定論的実行: `–frozen-lockfile` と共有ストアの組み合わせにより、どのビルド環境でも全く同一のバイナリ(ハードリンク)が生成されることが保証されます。

結論

`node_modules` を「各プロジェクトの管理下」に置く時代は終わりました。「全プロジェクトで共有されるグローバルな資源」として管理し、ハードリンクで最適化する。 このパラダイムへ移行することこそが、フロントエンド・アーキテクチャにおけるDevOpsの第一歩です。

今すぐあなたのCI/CD設定を見直し、`pnpm store` のマウントを検討してください。その数秒の積み重ねが、あなたのチームの年間数千時間の生産性を救うことになるのですから。

タイトルとURLをコピーしました