· pnpm / npm / package-manager
pnpm vs npm — điều thực sự thay đổi khi bạn chuyển đổi
Chuyển sang pnpm nếu bạn chạy monorepo hoặc cần CI cài đặt nhanh hơn. Ở lại npm nếu có phantom dependency chưa thể kiểm tra. Đây là sự khác biệt cụ thể.
Bởi Ethan
1.873 từ · 10 phút đọc
Chuyển sang pnpm nếu bạn chạy monorepo hoặc muốn cải thiện tốc độ cài đặt trên CI. Ở lại npm nếu bạn có phantom dependency chưa kiểm tra được và không có thời gian xử lý. Đó là kết luận — phần còn lại của bài viết giúp bạn xác định mình đang ở phía nào.
Bài viết này dành cho ai
Các developer JavaScript và TypeScript đang dùng npm và tự hỏi liệu chi phí chuyển đổi có đáng không. Nhóm làm monorepo sẽ được lợi nhiều nhất từ pnpm. Dev làm dự án đơn lẻ vẫn thấy cải thiện tốc độ, nhưng lợi thế về dung lượng đĩa và workspace mới là điểm pnpm thực sự tạo ra sự khác biệt.
Chúng tôi đã kiểm tra gì
Các con số benchmark lấy từ pnpm.io/benchmarks, cập nhật lần cuối ngày 2026-05-14. Đây là benchmark của chính pnpm, không phải từ bên thứ ba độc lập — hãy xem các hệ số nhân là định hướng, không phải sự thật tuyệt đối. Kịch bản thử nghiệm là một monorepo lớn (“nhiều file”), đây là workload quan trọng nhất nếu bạn đang đánh giá việc chuyển đổi.
Tốc độ
| Kịch bản | npm | pnpm | Tỷ lệ |
|---|---|---|---|
| Cold install (không cache, không lockfile, không node_modules) | 29.1s | 7.8s | Nhanh hơn 3.7× |
| Có cache + lockfile | 1.3s | 525ms | Nhanh hơn 2.5× |
| Có cache + lockfile + node_modules sẵn | 9.4s | 2.3s | Nhanh hơn 4.1× |
| Cập nhật sau khi lockfile thay đổi | 6.6s | 3.9s | Nhanh hơn 1.7× |
Khoảng cách lớn nhất ở cold install — trường hợp bạn gặp khi khởi động CI container mới hoặc máy developer mới. Kịch bản cache ấm (có cache + lockfile, không có node_modules) là trường hợp phổ biến trên CI: pnpm vẫn nhanh hơn 2.5× ở đây.
Một phát hiện ngược với trực giác: cache pnpm store trên CI không phải lúc nào cũng đáng. Cold install 7.8s của pnpm đã đủ nhanh đến mức việc khôi phục cache chậm có thể tốn thêm thời gian hơn là tiết kiệm. Hãy benchmark thời gian restore trước khi thêm store caching.
Dung lượng đĩa
pnpm lưu mỗi phiên bản package một lần duy nhất trong một store toàn cục theo địa chỉ nội dung (~/.pnpm-store trên Linux/Mac, %LOCALAPPDATA%\pnpm\store trên Windows), rồi hard-link vào node_modules của từng dự án.
Sự khác biệt cụ thể: nếu [email protected] có 100 file và [email protected] thay đổi 1 file, pnpm chỉ thêm 1 file vào store toàn cục. npm ghi 101 file mới — toàn bộ bản sao của phiên bản mới.
Trong một monorepo 20 package mà tất cả đều phụ thuộc vào React 18.3.1, npm cài React 20 lần (mỗi package một lần). pnpm cài một lần, hard-link phần còn lại. Không có con số “tiết kiệm X%” cố định — nó tỷ lệ thuận với số package chia sẻ cùng phiên bản — nhưng trong một monorepo dày đặc, con số này tích lũy nhanh.
Phantom dependency
Đây là phần hay làm migration thất bại.
npm (và Yarn 1.x) làm phẳng node_modules. Bất kỳ package nào được cài bởi một dependency đều nằm trong thư mục root chung và có thể truy cập từ code của bạn, dù bạn có khai báo hay không.
Ví dụ điển hình: dự án của bạn phụ thuộc vào express. Express phụ thuộc vào debug. Với npm, require('debug') hoạt động trong code vì debug đã được đưa lên cây phẳng chung. Nó không bao giờ có trong package.json của bạn. Nếu Express bỏ debug, code của bạn âm thầm bị lỗi.
pnpm cô lập từng package vào node_modules riêng của nó, chỉ chứa những gì package đó đã khai báo. require('debug') sẽ thất bại nếu debug không có trong package.json của bạn.
Những điểm hay gây lỗi khi migration:
- Toolchain Webpack/Babel — thường load plugin bằng cách duyệt cây phẳng
- Cấu hình Jest — các thiết lập transform và
moduleNameMapperđược viết với giả định cây phẳng - Script
postinstall— gọirequire()trên transitive dep của package anh em
Một trường hợp thực tế được ghi lại: webpack-dashboard dùng babel-traverse mà không khai báo. Hoạt động tốt trên npm, thất bại trên pnpm vì babel-traverse không có trong deps đã khai báo của webpack-dashboard.
Kiểm tra trước khi migrate. Chạy npx depcheck hoặc npx knip trên dự án của bạn. Các công cụ này phát hiện dependency chưa khai báo trước khi bạn phải debug lỗi runtime sau khi chuyển đổi.
Phương án dự phòng, theo thứ tự ưu tiên:
- Sửa code — thêm dep còn thiếu vào
package.json(cách đúng) .pnpmfile.mjs— inject dep còn thiếu lúc resolve mà không cần sửa filepackage.jsonbạn không sở hữupublic-hoist-pattern— đưa các pattern cụ thể lên rootnode_modules(ví dụ:['@babel/*', '*plugin*'])shamefully-hoist = true— đưa mọi thứ lên, layout phẳng giống npm; mất đi tính cô lập nhưng cho phép migrate từng bướcnodeLinker: hoisted— layout kiểu npm đầy đủ không có symlink; không có lỗi phantom dep nhưng cũng không có lợi ích về đĩa hay tốc độ
Ưu tiên dùng #1 khi có thể. #4 và #5 là giàn giáo migration, không phải đích đến.
Hỗ trợ workspace
pnpm dùng file pnpm-workspace.yaml ở root repo. npm dùng trường workspaces trong package.json. Về chức năng tương tự nhau — nhưng có một điểm khác biệt quan trọng.
Protocol workspace: là tính năng nổi bật của pnpm cho monorepo:
{
"dependencies": {
"@myapp/ui": "workspace:*"
}
}
Cái này chỉ resolve đến package local. Nó không bao giờ âm thầm fallback về registry nếu version không khớp. npm workspaces không có cơ chế kiểm tra tương đương. Trong một monorepo lớn với nhiều tham chiếu liên gói, điều này giúp phát hiện lớp bug mà package local cũ kỹ âm thầm được lấy từ registry.
Khi publish, pnpm tự động viết lại workspace:^1.0.0 thành semver range thực trong package.json đã publish. Không cần bước thủ công.
Một file pnpm-lock.yaml duy nhất ở root workspace bao phủ tất cả packages mặc định.
Tín hiệu chấp nhận từ các framework rất rõ ràng: Next.js, Vue, Vite, Nuxt, Astro, và Material UI đều dùng pnpm workspaces nội bộ.
Turborepo
Turborepo hỗ trợ pnpm ngay từ đầu. Điểm khác biệt duy nhất liên quan đến pnpm là flag cài đặt khi khởi tạo:
pnpm add turbo --save-dev --ignore-workspace-root-check
Pipeline tác vụ, caching, và remote cache hoạt động giống nhau trên tất cả package manager. Đây chỉ là sự khác biệt nhỏ, không phải vấn đề tương thích.
Lockfile và CI
pnpm-lock.yaml ghim phiên bản chính xác giống cách package-lock.json làm. pnpm tự động bật chế độ frozen-lockfile khi phát hiện CI=true — tương đương với npm ci. Bạn không cần nhớ flag này.
Một hành vi cần biết trước khi chuyển sang pnpm v11: nó sẽ thất bại nếu lockfile được tạo bởi phiên bản major pnpm mới hơn. Phiên bản pnpm trên CI và trên máy local cần đồng bộ. Giải pháp là pin phiên bản:
{
"packageManager": "[email protected]"
}
Cấu hình GitHub Actions được khuyên dùng:
- uses: pnpm/action-setup@v6
with:
version: 11
- uses: actions/setup-node@v4
with:
node-version: 20
cache: 'pnpm'
- run: pnpm install --frozen-lockfile
pnpm có tài liệu thiết lập CI cho GitHub Actions, GitLab CI, CircleCI, Azure Pipelines, Bitbucket Pipelines, Jenkins, Semaphore, và Travis CI.
Lộ trình migration từ npm sang pnpm
Chín bước, theo thứ tự:
1. Cài đặt pnpm:
corepack enable && corepack prepare pnpm@latest --activate
2. Nếu là monorepo, tạo pnpm-workspace.yaml ở root:
packages:
- 'packages/*'
- 'apps/*'
3. Chuyển đổi lockfile:
pnpm import
Đọc package-lock.json, tạo pnpm-lock.yaml.
4. Xóa các artifact cũ:
rm -rf node_modules package-lock.json
# Với monorepo: cũng xóa node_modules trong từng workspace package
5. Cài đặt:
pnpm install
6. Xử lý phantom dep. Nếu cài đặt thành công nhưng xuất hiện lỗi runtime, chạy npx depcheck, thêm các package được phát hiện vào package.json, rồi cài lại.
7. Pin phiên bản:
{
"packageManager": "[email protected]"
}
8. Cập nhật CI (xem đoạn GitHub Actions ở trên).
9. Cập nhật Dockerfile:
# Thay thế
RUN npm ci
# Bằng
RUN pnpm install --frozen-lockfile
Một lưu ý về rollback: giữ package-lock.json trong version control cho đến khi bạn hoàn toàn tự tin. pnpm và npm không thể cùng quản lý một node_modules. Khi đã quyết định, xóa package-lock.json.
Hosting
Vercel và Railway đều tự phát hiện pnpm qua trường packageManager trong package.json. Vercel đọc nó lúc build và cấu hình lệnh cài đặt mà không cần thay đổi cấu hình. Render cũng hỗ trợ pnpm, dù chúng tôi chưa có affiliate link cho Render tại thời điểm viết bài.
Kết luận
Chuyển sang pnpm nếu:
- Bạn quản lý monorepo — chỉ riêng protocol
workspace:và lockfile chung duy nhất đã đáng - Bạn đang tốn thời gian CI cho npm install và muốn một giải pháp có cấu trúc thay vì thêm caching
- Bạn chạy nhiều dự án trên một máy và gặp giới hạn đĩa do
node_modulestrùng lặp
Ở lại npm nếu:
- Bạn có codebase legacy lớn với phantom dependency chưa thể kiểm tra ngay
- Dự án của bạn chỉ có một package, nhóm nhỏ, và tốc độ cài đặt không phải điểm nghẽn
Migration có thể rollback — pnpm import chuyển đổi npm lockfile, và bạn có thể tái tạo package-lock.json bằng cách chạy npm install sau khi xóa pnpm. Chạy npx depcheck trước, xử lý những gì nó tìm thấy, phần còn lại của migration chỉ là thao tác cơ học.
Nếu bạn cũng đang đánh giá các JavaScript runtime, hãy đọc Bun vs Node.js — Bun đi kèm package manager riêng, khiến câu hỏi pnpm vs npm không còn liên quan nếu bạn chuyển sang Bun. Nếu bạn đang thiết lập pipeline build cho monorepo, Vite vs Webpack đề cập đến quyết định về bundler thường đến ngay sau quyết định về package manager.
Lưu ý
Benchmark lấy từ trang benchmark của chính pnpm, không phải từ một phòng lab độc lập. Thứ tự tương đối (pnpm nhanh hơn, tiết kiệm đĩa hơn) đã được xác lập rõ ràng. Các hệ số nhân cụ thể nên được coi là ước lượng, đặc biệt nếu workload của bạn khác đáng kể so với monorepo nhiều package lớn.
shamefully-hoist và nodeLinker: hoisted là các phương án dự phòng, nhưng chúng loại bỏ phần lớn lý do để chuyển đổi. Nếu bạn cần chúng vĩnh viễn thay vì chỉ trong giai đoạn chuyển tiếp, hãy xem xét lại liệu chi phí migration có xứng đáng không.