pnpm là gì? pnpm là trình quản lý gói (package manager) hiệu năng cao cho Node.js, viết tắt của "Performant NPM". Công cụ này được thiết kế để thay thế npm và Yarn bằng cách rút ngắn thời gian cài đặt và dùng tài nguyên hệ thống hiệu quả hơn.
Khác biệt cốt lõi của pnpm nằm ở cách lưu trữ gói theo nội dung (content-addressable store) và cấu trúc node_modules không phẳng (non-flat). Thay vì sao chép các gói phụ thuộc vào từng dự án, pnpm gỡ nút thắt cổ chai I/O bằng liên kết cứng (hard link) và liên kết tượng trưng (symlink).
Từ góc độ kỹ thuật, pnpm xử lý những vấn đề tồn đọng lâu năm của hệ sinh thái Node.js: hàng nghìn tệp trùng lặp lấp đầy ổ đĩa, và rủi ro từ các phụ thuộc ma. Nếu bạn đang quản lý dự án quy mô lớn hoặc một monorepo, pnpm cho bạn mức độ minh bạch và tốc độ mà npm không đáp ứng được.

pnpm là gì?
pnpm được Zoltan Kochan khởi tạo năm 2016, với mục tiêu giải quyết hai vấn đề lớn nhất của npm: lãng phí dung lượng ổ đĩa, và cấu trúc node_modules lỏng lẻo. pnpm hướng tới một hạ tầng quản lý gói tin cậy và minh bạch, thay cho hàng nghìn bản sao vật lý của cùng một gói phụ thuộc rải khắp các dự án.
Các cột mốc gần đây cho thấy một tư duy "secure by default" (an toàn theo mặc định). Từ phiên bản v10, pnpm chặn các script cài đặt theo mặc định để ngăn mã độc. Phiên bản v11 đánh dấu bước chuyển sang ESM (ECMAScript Modules) thuần túy và yêu cầu môi trường Node.js 22 trở lên.
Lộ trình hiện tại hướng tới phiên bản v12 với dự án "pacquet" — một nỗ lực viết lại hoàn toàn công cụ bằng ngôn ngữ Rust. Mục tiêu của v12 không phải thay đổi cách bạn sử dụng, mà là thay lõi thực thi bằng mã máy bản địa (native binary): bỏ chi phí khởi động Node.js và xử lý song song bằng Rust để cài đặt nhanh hơn nhiều lần.
pnpm lưu gói khác npm như thế nào?
Cơ chế lưu trữ của pnpm dựa trên content-addressable store. Thay vì lưu gói trong thư mục dự án, pnpm lưu tất cả các gói tại một vị trí duy nhất trên ổ đĩa. Khi cài đặt, pnpm dùng hard link để trỏ từ thư mục dự án vào kho lưu trữ trung tâm. Dù bạn có hàng trăm dự án dùng chung một phiên bản gói, dung lượng vật lý tiêu tốn vẫn chỉ tương đương một dự án.

Thành phần quan trọng thứ hai là virtual store (kho lưu trữ ảo) nằm trong node_modules/.pnpm. Khác với cách làm phẳng (hoisting) của npm, pnpm dùng cấu trúc thư mục lồng nhau: một gói nằm tại đường dẫn dạng foo@1.0.0/node_modules/foo. Thiết kế này nhằm hai mục đích kiến trúc:
- Cho phép gói tự import chính nó. Mã nguồn bên trong gói gọi được các tệp của chính nó qua cơ chế phân giải của Node.js.
- Tránh symlink vòng (circular symlink). Các phụ thuộc của một gói được đặt cùng cấp với gói đó thay vì lồng sâu vô tận, giúp Node.js tìm thấy phụ thuộc trong thư mục cha mà không sinh ra vòng lặp.
Minh họa cấu trúc thư mục của pnpm:
node_modules
└── .pnpm
├── bar@1.0.0
│ └── node_modules
│ └── bar
└── foo@1.0.0
└── node_modules
└── fooTừ v11, chỉ mục kho lưu trữ chuyển từ hàng triệu tệp JSON riêng lẻ sang một cơ sở dữ liệu SQLite duy nhất tại $STORE/index.db, dùng chế độ WAL (write-ahead logging) cho truy cập đồng thời. pnpm cũng ghi thẳng tệp vào đường dẫn theo nội dung thay vì qua bước "tệp tạm rồi đổi tên", tiết kiệm khoảng 30.000 lệnh gọi hệ thống rename mỗi lần cài sạch. Cấu trúc này vừa tiết kiệm ổ đĩa, vừa giảm thao tác I/O nặng nề.
pnpm nhanh hơn npm tới mức nào?
Hiệu năng của pnpm không chỉ đến từ việc giảm I/O, mà còn từ cơ chế cài đặt ba giai đoạn chạy song song cho từng gói: resolution (giải quyết phụ thuộc), calculation (tính toán cấu trúc) và linking (liên kết gói). npm phải chờ giải quyết xong toàn bộ đồ thị phụ thuộc; pnpm chạy các bước này ngay khi xác định được một gói cụ thể.

Có hai phép đo thực tế, và chúng đo hai thứ khác nhau nên không gộp chung được. Phép đo thứ nhất là một monorepo lớn chạy trong CI, đo năm 2022:
| Trình quản lý gói | Không có cache | Có cache |
|---|---|---|
| Yarn 2 | 6 phút 31 giây | 1 phút 11 giây |
| Yarn 3 (đã tối ưu) | 1 phút 10 giây | 45 giây |
| pnpm | 58 giây | 24 giây |
Phép đo thứ hai so sánh chính lõi thực thi của pnpm, giữa v11 chạy trên Node.js và v12 viết bằng Rust:
| Kịch bản | pnpm v11 (Node) | pnpm v12 (Rust) |
|---|---|---|
| Cài sạch, không cache | 6,5 giây | 2,2 giây |
| Cài lại khi đã có sẵn (warm) | 381 ms | 12 ms |
Cải thiện trong môi trường CI (Continuous Integration) là rất đáng kể: với hệ thống lớn, kéo thời gian cài đặt từ hơn sáu phút xuống dưới một phút tiết kiệm trực tiếp chi phí hạ tầng và rút ngắn vòng phản hồi của quy trình phát triển. Với v12, lần cài lại chỉ mất 12 ms vì lõi Rust không phải khởi động runtime Node.js — nó chỉ xác minh lại cây phụ thuộc đã có.
Phụ thuộc ma là gì và pnpm chặn chúng ra sao?
Phụ thuộc ma (phantom dependency) xảy ra khi mã nguồn của bạn import được những gói không hề khai báo trong package.json. npm và Yarn Classic gây ra lỗi này qua cơ chế hoisting — đẩy tất cả phụ thuộc bắc cầu lên gốc node_modules. Rủi ro nằm ở chỗ: mã chạy được hôm nay sẽ chết ngay khi phụ thuộc bắc cầu đó đổi phiên bản hoặc bị một gói cha gỡ bỏ.

pnpm chặn lỗi này bằng cách chỉ đưa các phụ thuộc trực tiếp vào thư mục gốc node_modules. Toàn bộ phụ thuộc bắc cầu nằm kín trong .pnpm. Nếu bạn import một gói chưa khai báo, Node.js báo lỗi ngay vì đường dẫn phân giải bị chặn. Nói cách khác, package.json trở thành nguồn sự thật duy nhất cho đồ thị phụ thuộc.
Kỷ luật này đôi khi quá nghiêm với các dự án cũ. Khi cần, bạn đặt shamefully-hoist=true để mô phỏng hành vi của npm, đảm bảo tương thích với những công cụ chưa hỗ trợ symlink, mà vẫn giữ được lợi ích từ kho lưu trữ tập trung. Một lựa chọn khác là node-linker=hoisted, đưa bố cục về đúng kiểu npm — đổi lại bạn mất tính nghiêm ngặt của pnpm.
pnpm làm gì được trong monorepo?
Trong kiến trúc monorepo — một kho mã duy nhất chứa nhiều gói con — pnpm mạnh nhờ tính năng workspace có sẵn. Toàn bộ các gói nội bộ được khai báo trong tệp pnpm-workspace.yaml, cho phép quản lý hàng trăm gói tập trung một chỗ.

packages:
- "packages/*"
- "apps/*"Thế mạnh của pnpm trong monorepo là giao thức workspace:. Thay vì tải gói từ registry công cộng, bạn dùng "foo": "workspace:*" để buộc pnpm liên kết thẳng tới mã nguồn cục bộ; khi xuất bản, các tham chiếu này tự động chuyển về dải semver chuẩn. pnpm còn có catalog — cách khai báo phiên bản dùng chung cho toàn workspace tại một nơi duy nhất, thay vì lặp lại cùng một chuỗi phiên bản trong hàng chục tệp package.json.
Tính năng shared-workspace-lockfile tạo một tệp lock duy nhất cho cả repository. Nó giảm đáng kể thời gian cài đặt và đảm bảo mọi thành viên trong đội dùng đúng cùng một phiên bản của mọi gói. Cờ --filter (viết tắt -F) thì khoanh vùng lệnh: pnpm -F "@my-org/ui-kit..." build dựng ui-kit cùng tất cả các gói đang phụ thuộc vào nó.
Vì sao pnpm chặn script cài đặt của gói?
Từ phiên bản v10.0.0, pnpm chặn các lifecycle script như preinstall và postinstall theo mặc định. Thay đổi này bảo vệ bạn khỏi tấn công chuỗi cung ứng — chẳng hạn vụ Rspack, nơi mã độc được phát tán qua một script postinstall.

pnpm chuyển từ mô hình "tin cậy mù quáng" sang "chủ động cho phép". Để script chạy được, bạn phải khai báo rõ những gói mình tin:
- Ở v10, dùng trường
pnpm.onlyBuiltDependencies. - Ở v11, cấu hình này đổi thành
allowBuildstrongpnpm-workspace.yaml.
v11 còn đặt minimumReleaseAge mặc định là 1440 phút (một ngày): một gói vừa được xuất bản sẽ không phân giải được cho tới khi đủ tuổi, chặn các đợt tấn công chớp nhoáng ngay sau khi kẻ tấn công đẩy phiên bản độc lên registry. Lệnh pnpm approve-builds cho bạn duyệt và cho phép từng script một cách tương tác, rồi ghi lựa chọn đó vào cấu hình chung của đội.
pnpm, Yarn hay Bun — nên chọn cái nào?
Lựa chọn phụ thuộc vào triết lý vận hành của dự án:
| Tiêu chí | pnpm | Yarn (Berry) | Bun |
|---|---|---|---|
| Triết lý | Tiết kiệm ổ đĩa, an toàn | Plug'n'Play, tái lập được | Tốc độ cực hạn (native) |
| Cấu trúc | Symlink / hard link | PnP (không có node_modules) | Tương thích npm |
| Monorepo | Mạnh (catalog, filter) | Cao cấp (constraints) | Cơ bản |
| Tốc độ | Rất nhanh (lõi Rust ở v12) | Trung bình | Cực nhanh |
pnpm là lựa chọn an toàn cho hệ thống lớn cần giữ node_modules truyền thống nhưng vẫn muốn tối ưu tốc độ. Bun hiện dẫn đầu về tốc độ cài đặt nhờ viết bằng Zig, còn pnpm v12 với lõi Rust đang thu hẹp khoảng cách. Yarn Berry với PnP là giải pháp sáng tạo, nhưng thường gây khó khi tích hợp với các công cụ chưa hỗ trợ cơ chế phân giải không dùng tệp — và nếu đội bạn cần "zero install" (đóng gói phụ thuộc thẳng vào Git) thì đó lại đúng là thứ Yarn làm tốt nhất.
Nên bắt đầu với pnpm từ đâu?
Nếu hệ thống của bạn đang gặp vấn đề dung lượng ổ đĩa hoặc CI chạy quá chậm, chuyển sang pnpm là quyết định kiến trúc đúng. Cài bằng npm install -g pnpm, rồi chạy pnpm import để chuyển tệp lock hiện có sang pnpm-lock.yaml.
Một lưu ý nếu bạn đang dùng v11: .npmrc giờ chỉ dành cho xác thực và registry. Mọi cài đặt khác phải chuyển vào pnpm-workspace.yaml hoặc tệp config.yaml toàn cục — bỏ qua bước này là lý do phổ biến nhất khiến một dự án cũ nâng cấp lên v11 rồi thấy cấu hình của mình lặng lẽ mất tác dụng.
Tài liệu tham khảo
- Motivation | pnpm
- Symlinked node_modules structure | pnpm
- Workspace | pnpm
- pnpm 11.0 | pnpm
- pnpm v12 Is Being Rewritten in Rust — Dennis Morello
- pnpm 10.0.0 Blocks Lifecycle Scripts by Default — Socket
- A story of how we migrated to pnpm — ‹div›RIOTS
- PNPM vs. Bun Install vs. Yarn Berry — Better Stack Community