跳轉到內容

pnpm 性能優化與 monorepo 最佳實踐 2026 | 包管理完全指南

pnpm 性能優化與 monorepo 最佳實踐

pnpm 憑藉其獨特的內容尋址存儲和符號鏈接機制,在安裝速度和磁盤空間方面比 npm/yarn 有數量級的提升。本文將深入 pnpm 核心原理、性能優化技巧、monorepo 架構設計及 CI/CD 最佳實踐。


一、pnpm 核心原理

1.1 內容尋址存儲

pnpm 最大的創新是使用內容尋址存儲(Content-Addressable Storage)

~/.pnpm-store/                    # 全局存儲目錄
├── files/
│   ├── 01/234567...              # 按文件內容 hash 存儲
│   ├── ab/cdef12...              # 相同內容只存一份
│   └── ...
├── index.json                    # 包索引
└── metadata.json                 # 元數據

核心優勢:

  • 相同版本的包在全局只存一份
  • 不同版本的包只存差異文件
  • 磁盤空間節省 70%-90%

1.2 符號鏈接機制

node_modules/
├── .pnpm/
│   ├── react@18.2.0/
│   │   └── node_modules/
│   │       ├── react/            # 硬鏈接到 store
│   │       └── loose-envify/     # 依賴軟鏈接到 .pnpm/loose-envify@*
│   ├── loose-envify@1.4.0/
│   │   └── node_modules/
│   │       └── loose-envify/
│   └── ...
└── react -> .pnpm/react@18.2.0/node_modules/react  # 頂層軟鏈接

1.3 與 npm/yarn 的對比

特性npmyarn (classic)pnpm
全局存儲❌ 每項目一份❌ 每項目一份✅ 全局唯一
幽靈依賴✅ 可訪問✅ 可訪問❌ 默認禁止
安裝速度基準快 2x快 5-10x
磁盤佔用基準與 npm 相當節省 70%-90%
monorepo 支持workspacesworkspacesbuilt-in 強大
嚴格度高(默認嚴格)

二、性能優化技巧

2.1 加速安裝

bash
# 1. 啟用預打包(默認已啟用)
pnpm config set side-effects-cache true

# 2. 開啟離線模式(已有緩存時)
pnpm install --offline

# 3. 僅安裝生產依賴
pnpm install --prod

# 4. 跳過可選依賴
pnpm install --no-optional

# 5. 併發數調優(默認 4)
pnpm config set network-concurrency 10

# 6. 啟用重試
pnpm config set fetch-retries 5
pnpm config set fetch-retry-mintimeout 10000

2.2 配置文件優化

yaml
# .npmrc
# 鏡像源加速
registry=https://registry.npmmirror.com

# 存儲目錄(可選,默認 ~/.local/share/pnpm/store)
store-dir=~/.pnpm-store

# 嚴格模式(推薦)
strict-peer-dependencies=false
auto-install-peers=true

# 性能優化
side-effects-cache=true
link-workspace-packages=true

# 並行數
network-concurrency=8
child-concurrency=10

# 輸出風格
reporter=default

2.3 清理與優化

bash
# 查看存儲使用情況
pnpm store status

# 清理未使用的包
pnpm store prune

# 查看佔用空間
du -sh $(pnpm store path)

# 完整清理(慎用)
pnpm store clear

# 驗證存儲完整性
pnpm store verify

三、monorepo 架構設計

3.1 項目結構

my-monorepo/
├── packages/
│   ├── ui/                       # UI 組件庫
│   │   ├── src/
│   │   ├── package.json
│   │   └── tsconfig.json
│   ├── utils/                    # 工具函數庫
│   │   ├── src/
│   │   └── package.json
│   ├── hooks/                    # 自定義 hooks
│   │   ├── src/
│   │   └── package.json
│   └── eslint-config/            # 共享 ESLint 配置
│       ├── index.js
│       └── package.json
├── apps/
│   ├── web/                      # Web 應用
│   │   ├── src/
│   │   └── package.json
│   ├── admin/                    # 管理後臺
│   │   ├── src/
│   │   └── package.json
│   └── docs/                     # 文檔站點
│       └── package.json
├── pnpm-workspace.yaml           # Workspace 配置
├── package.json
├── .npmrc
├── tsconfig.base.json
└── pnpm-lock.yaml

3.2 pnpm-workspace.yaml 配置

yaml
# pnpm-workspace.yaml
packages:
  - 'apps/*'
  - 'packages/*'
  - 'config/*'

# 排除特定目錄
exclude-non-workspace: true

3.3 根 package.json

json
{
  "name": "my-monorepo",
  "private": true,
  "packageManager": "pnpm@9.0.0",
  "scripts": {
    "build": "pnpm -r build",
    "build:web": "pnpm --filter web build",
    "dev": "pnpm --parallel --filter \"./apps/*\" dev",
    "lint": "pnpm -r lint",
    "test": "pnpm -r test",
    "typecheck": "pnpm -r typecheck",
    "clean": "pnpm -r exec rm -rf dist node_modules/.cache"
  },
  "devDependencies": {
    "typescript": "^5.4.0",
    "eslint": "^8.57.0",
    "prettier": "^3.2.0",
    "vitest": "^1.6.0"
  }
}

四、Filter 過濾語法詳解

4.1 基礎過濾

bash
# 指定包名
pnpm --filter my-app build

# 多包過濾
pnpm --filter app-a --filter app-b test

# 通配符匹配
pnpm --filter "@scope/*" build
pnpm --filter "*-ui" lint

4.2 依賴關係過濾

bash
# 包及其所有依賴(正向)
pnpm --filter web... build

# 包及其所有被依賴(反向)
pnpm --filter ...ui test

# 雙向:包 + 依賴 + 被依賴
pnpm --filter ...web... lint

4.3 目錄過濾

bash
# 指定目錄
pnpm --filter "./apps/*" dev

# 排除目錄
pnpm --filter "!./apps/legacy" build

4.4 變更過濾

bash
# 基於 git 變更
pnpm --filter "[HEAD~1]" build

# 基於分支
pnpm --filter "[main]" test

# 基於 tag
pnpm --filter "[v1.0.0]" lint

4.5 並行執行

bash
# 並行運行所有匹配包的腳本
pnpm --parallel --filter "./packages/*" dev

# 限制併發數
pnpm --parallel --concurrency 4 --filter "./apps/*" build

五、幽靈依賴治理

5.1 什麼是幽靈依賴

javascript
// package.json 中只聲明瞭 lodash
{ "dependencies": { "lodash": "^4.17.21" } }

// 但代碼中使用了 axios(未在 package.json 聲明)
// 之所以能用,是因為 axios 是其他包的依賴
import axios from 'axios'  // ❌ 幽靈依賴

5.2 pnpm 的解決方案

pnpm 默認使用嚴格模式,不允許訪問未聲明的依賴:

node_modules/
├── .pnpm/         # 所有真實包都在這裡
└── lodash/        # 只有聲明過的包會軟鏈接到頂層

如果代碼訪問了未聲明的依賴,會直接報錯 Cannot find module

5.3 處理幽靈依賴的方法

方法 1:在 package.json 中顯式聲明(推薦)

json
{
  "dependencies": {
    "axios": "^1.6.0",   // 顯式添加
    "lodash": "^4.17.21"
  }
}

方法 2:使用 .pnpmfile.cjs 修補

javascript
// .pnpmfile.cjs
function readPackage(pkg) {
  // 為某個包補充缺失的依賴聲明
  if (pkg.name === 'some-bad-package') {
    pkg.dependencies = {
      ...pkg.dependencies,
      'axios': '^1.6.0'
    }
  }
  return pkg
}

module.exports = { hooks: { readPackage } }

方法 3:使用 public-hoist-pattern(臨時方案)

ini
# .npmrc
# 提升某些包到頂層 node_modules
public-hoist-pattern[]=*typescript*
public-hoist-pattern[]=*eslint*
public-hoist-pattern[]=*prettier*

5.4 檢測幽靈依賴

bash
# 安裝 knip 工具檢測未使用/未聲明的依賴
pnpm add -D knip

# 運行檢測
pnpm knip

六、版本管理與發佈

6.1 使用 Changesets 管理版本

bash
# 安裝
pnpm add -D @changesets/cli -w

# 初始化
pnpm changeset init
bash
# 添加變更集
pnpm changeset

# 消耗變更集(更新版本和 changelog)
pnpm changeset version

# 發佈
pnpm changeset publish

6.2 changeset 配置

json
// .changeset/config.json
{
  "$schema": "https://unpkg.com/@changesets/config@3.0.0/schema.json",
  "changelog": "@changesets/cli/changelog",
  "commit": false,
  "fixed": [["@my-org/ui", "@my-org/utils"]],
  "linked": [],
  "access": "restricted",
  "baseBranch": "main",
  "updateInternalDependencies": "patch",
  "ignore": ["@my-org/docs", "@my-org/legacy-app"]
}

6.3 workspace 協議

json
// 包內引用 workspace 中的其他包
{
  "name": "@my-org/web",
  "dependencies": {
    "@my-org/ui": "workspace:*",
    "@my-org/utils": "workspace:^1.2.0"
  }
}

七、CI/CD 最佳實踐

7.1 GitHub Actions 配置

yaml
# .github/workflows/ci.yml
name: CI

on:
  push:
    branches: [main]
  pull_request:
    branches: [main]

jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
        with:
          fetch-depth: 0

      - name: Setup pnpm
        uses: pnpm/action-setup@v3
        with:
          version: 9
          run_install: false

      - name: Setup Node.js
        uses: actions/setup-node@v4
        with:
          node-version: 20
          cache: 'pnpm'

      - name: Get pnpm store directory
        id: pnpm-cache
        shell: bash
        run: echo "STORE_PATH=$(pnpm store path)" >> $GITHUB_OUTPUT

      - name: Setup pnpm cache
        uses: actions/cache@v4
        with:
          path: ${{ steps.pnpm-cache.outputs.STORE_PATH }}
          key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }}
          restore-keys: |
            ${{ runner.os }}-pnpm-store-

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

      - name: Build
        run: pnpm --filter web build

      - name: Test
        run: pnpm --filter web test

7.2 增量構建優化

yaml
- name: Turbo build
  uses: dtinth/setup-github-actions-caching-for-turbo@v1

- name: Build
  run: pnpm build --filter="...[main]"

7.3 Docker 鏡像優化

dockerfile
# 多階段構建
FROM node:20-alpine AS base
RUN corepack enable && corepack prepare pnpm@9 --activate

# deps 階段
FROM base AS deps
WORKDIR /app
COPY pnpm-lock.yaml pnpm-workspace.yaml package.json ./
COPY packages/ui/package.json ./packages/ui/
COPY apps/web/package.json ./apps/web/
RUN pnpm install --frozen-lockfile

# build 階段
FROM base AS builder
WORKDIR /app
COPY --from=deps /app/node_modules ./node_modules
COPY --from=deps /app/pnpm-lock.yaml ./
COPY . .
RUN pnpm --filter web build

# runtime 階段
FROM nginx:alpine AS runner
COPY --from=builder /app/apps/web/dist /usr/share/nginx/html
EXPOSE 80

八、常見問題與解決方案

8.1 peer dependencies 問題

ini
# .npmrc
# 自動安裝 peer 依賴
auto-install-peers=true

# 忽略 peer 依賴警告(不推薦)
strict-peer-dependencies=false

8.2 私有包訪問

bash
# 設置私有源認證
pnpm config set @my-org:registry https://npm.pkg.github.com
pnpm config set -- '//npm.pkg.github.com/:_authToken' "YOUR_TOKEN"

# 或者使用 .npmrc
@my-org:registry=https://npm.pkg.github.com
//npm.pkg.github.com/:_authToken=${GITHUB_TOKEN}

8.3 切換到 pnpm

bash
# 1. 刪除舊的 node_modules 和 lock 文件
rm -rf node_modules package-lock.json yarn.lock

# 2. 生成 pnpm-lock.yaml
pnpm import package-lock.json   # 從 npm 遷移
pnpm import yarn.lock            # 從 yarn 遷移

# 3. 安裝
pnpm install

# 4. 檢查幽靈依賴
pnpm ls --depth 0

8.4 包發佈調試

bash
# 本地預覽打包內容
pnpm pack

# 鏈接本地包到全局
cd packages/my-package
pnpm link --global

# 在測試項目中使用
cd ../test-project
pnpm link --global my-package

# 取消鏈接
pnpm unlink --global my-package

九、高級配置

9.1 全局 packageExtensions

yaml
# .pnpmfile.cjs
module.exports = {
  hooks: {
    readPackage(pkg) {
      // 為所有 react 包補充 peer 依賴
      if (pkg.name === 'some-lib' && !pkg.peerDependencies?.react) {
        pkg.peerDependencies = {
          ...pkg.peerDependencies,
          react: '^18.0.0'
        }
      }
      return pkg
    }
  }
}

9.2 僅允許 pnpm

package.json 中強制使用 pnpm:

json
{
  "packageManager": "pnpm@9.0.0",
  "scripts": {
    "preinstall": "npx only-allow pnpm"
  }
}

十、總結

  • ✅ 理解 pnpm 內容尋址存儲與符號鏈接原理
  • ✅ 掌握性能優化技巧(加速安裝、緩存、併發控制)
  • ✅ monorepo 架構設計與 Workspace 配置
  • ✅ Filter 過濾語法(包名、依賴、目錄、git 變更)
  • ✅ 幽靈依賴治理與嚴格模式
  • ✅ Changesets 版本管理與發佈
  • ✅ CI/CD 最佳實踐(GitHub Actions、Docker 優化)
  • ✅ 常見問題排查與解決方案
  • ✅ 高級配置(packageExtensions、僅允許 pnpm)

pnpm 不僅僅是更快的包管理器,它的嚴格模式和 monorepo 能力讓前端工程化更加規範可靠。


相關閱讀:

最後更新於: