跳至主要內容

pnpm 取代 npm 的理由:更快、更省空間、更嚴格

pnpm 取代 npm 的理由:更快、更省空間、更嚴格

如果你每天都在用 npm,你一定遇過這些痛點:node_modules 吃掉幾 GB 硬碟空間、安裝速度慢、幽靈依賴(phantom dependencies)導致的奇怪 bug。pnpm 針對這些問題提出了一套完整的解決方案,而且幾乎完全相容 npm 的使用方式。

npm 的三大問題

問題一:磁碟空間浪費

假設你有 10 個專案,每個都用到 React:

project-a/node_modules/react/  # 一份 React
project-b/node_modules/react/  # 又一份 React(相同版本)
project-c/node_modules/react/  # 再一份 React
...

npm 和 Yarn 在每個專案中都複製一份套件,浪費大量磁碟空間。

問題二:安裝速度慢

每次安裝都要從 npm registry 下載,即使你昨天才下載過相同的套件。

問題三:幽靈依賴(Phantom Dependencies)

your-project
└── node_modules/
    ├── express/
    └── lodash/  ← express 的依賴,但你的程式碼直接 require('lodash')

npm 將所有依賴提升(hoist)到 node_modules 頂層,你的程式碼可以直接 require 你沒有宣告的套件。這很危險——一旦 express 升級並移除 lodash,你的程式就炸了。

pnpm 的解決方案

全域內容可定址儲存(Content-Addressable Store)

pnpm 在你的系統中維護一個全域儲存(通常在 ~/.pnpm-store):

~/.pnpm-store/
└── v3/
    └── files/
        ├── 00/ (hash 前兩位)
        │   └── abc123... (檔案 hash)
        ├── 01/
        └── ...

每個套件的每個檔案只儲存一次。相同內容的不同套件版本,共用相同的底層檔案。

project-a/node_modules/react → ~/.pnpm-store/.../react/index.js (硬連結)
project-b/node_modules/react → ~/.pnpm-store/.../react/index.js (同一個檔案!)

所有專案的 node_modules 都指向同一個全域儲存,不額外佔用磁碟空間。

符號連結(Symlinks)隔離依賴

project/node_modules/
├── .pnpm/                    # 真實位置
│   ├── express@4.18.0/
│   │   └── node_modules/
│   │       ├── express/
│   │       └── lodash/       # express 的依賴在這裡
│   └── react@18.2.0/
│       └── node_modules/
│           └── react/
├── express -> .pnpm/express@4.18.0/node_modules/express
└── react   -> .pnpm/react@18.2.0/node_modules/react

頂層 node_modules 只有你在 package.json 中宣告的套件,杜絕幽靈依賴。

安裝 pnpm

# macOS / Linux
curl -fsSL https://get.pnpm.io/install.sh | sh

# 或使用 npm 安裝(諷刺)
npm install -g pnpm

# 確認安裝
pnpm --version

基本使用

pnpm 的指令與 npm 幾乎相同:

# 安裝所有依賴
pnpm install

# 新增套件
pnpm add react vue
pnpm add -D typescript vite

# 移除套件
pnpm remove lodash

# 執行腳本
pnpm run dev
pnpm dev  # 可省略 run

# 全域安裝
pnpm add -g typescript

# 查看過時套件
pnpm outdated

# 更新套件
pnpm update

Monorepo 支援(Workspaces)

pnpm 原生支援 monorepo,效率遠優於 npm workspaces:

# pnpm-workspace.yaml
packages:
  - 'packages/*'
  - 'apps/*'
monorepo/
├── pnpm-workspace.yaml
├── package.json
├── packages/
│   ├── ui/
│   │   └── package.json  { "name": "@company/ui" }
│   └── utils/
│       └── package.json  { "name": "@company/utils" }
└── apps/
    ├── web/
    │   └── package.json  { "dependencies": { "@company/ui": "workspace:*" } }
    └── admin/
        └── package.json
# 在特定 workspace 執行指令
pnpm --filter @company/ui build
pnpm --filter web dev

# 在所有 workspace 執行指令
pnpm -r build
pnpm -r --parallel test

# 安裝套件到特定 workspace
pnpm add react --filter web

.npmrc 設定

# .npmrc
# 嚴格模式:禁止存取未宣告的依賴
hoist=false

# 或更嚴格:使用 node-linker
node-linker=isolated

# 自動安裝 peer dependencies
auto-install-peers=true

# 使用 workspace 中的本地套件
prefer-workspace-packages=true

速度比較

以安裝一個典型的 Next.js 專案為例:

工具 冷安裝(無快取) 暖安裝(有快取)
npm ~60s ~20s
Yarn ~35s ~10s
pnpm ~15s ~3s

磁碟空間比較

10 個各自包含 React + Vue + lodash 的專案:

工具 總磁碟使用量
npm ~2.1 GB
Yarn ~1.9 GB
pnpm ~350 MB

從 npm 遷移

# 刪除現有 node_modules 和 lock file
rm -rf node_modules
rm package-lock.json

# 用 pnpm 安裝
pnpm install

# 將 package-lock.json 換成 pnpm-lock.yaml
# 記得更新 .gitignore
# .gitignore
node_modules/
# 保留 pnpm-lock.yaml(加入版本控制)

CI/CD 設定

# GitHub Actions
- name: Install pnpm
  uses: pnpm/action-setup@v2
  with:
    version: 8

- name: Install dependencies
  run: pnpm install --frozen-lockfile

總結

pnpm 解決了 npm 多年來的三大問題:磁碟空間浪費、安裝速度慢和幽靈依賴。它的遷移成本極低,指令幾乎與 npm 相同。對於有多個 Node.js 專案的開發者,pnpm 的磁碟空間節省效果立竿見影。強烈建議所有新專案直接採用 pnpm,舊專案也值得遷移。

分享這篇文章