Modern JavaScript development is unthinkable without package managers. React, TypeScript, ESLint, Webpack, Vite, Jest, Cypress — almost everything you use comes from npm packages.
But there isn’t just one way to manage them.
Today, the three most common tools are:
- npm — the default, ships with Node.js
- Yarn — introduced faster installs and better workflows
- pnpm — focuses on speed and disk efficiency with a clever storage model
In this post, we’ll walk through:
- What a package manager actually does
- The core concepts that npm, pnpm, and Yarn share
- How each tool behaves, with concrete examples
- Strengths and trade-offs of each
- How to pick one for your projects and monorepos
1. What Does a Package Manager Do?
At a high level, a package manager:
- Installs dependencies listed in your
package.json - Resolves and pins exact versions in a lockfile
- Makes installed packages available to your application, usually through
node_modulesor Yarn Plug'n'Play - Runs scripts defined in
package.json(lint, test, build, etc.) - Publishes packages to registries like npmjs.com
A minimal package.json might look like:
{
"name": "my-app",
"version": "1.0.0",
"scripts": {
"dev": "vite",
"build": "vite build",
"test": "vitest"
},
"dependencies": {
"react": "^18.3.0",
"react-dom": "^18.3.0"
},
"devDependencies": {
"vite": "^5.0.0",
"vitest": "^2.0.0"
}
}All three package managers understand this file. The differences lie in:
- Install speed
- Disk space usage
- Lockfile format
- Workspace/monorepo support
- Extra features and workflows
2. Core Concepts: Same Ideas, Different Implementations
Before we dive into each tool, let’s align on concepts they all share.
2.1 Dependencies and Version Ranges
In package.json:
"dependencies": {
"axios": "^1.7.0",
"zustand": "~5.0.0"
}Common version range operators:
-
^1.7.0- Accepts
>=1.7.0 <2.0.0 - Allows new features and bug fixes, but blocks breaking changes
- Accepts
-
~5.0.0- Accepts
>=5.0.0 <5.1.0 - Only patch updates; more conservative
- Accepts
An exact version:
"typescript": "5.6.3"This pins one release. A lockfile records the resolved dependency graph, but reproducible CI also depends on using an install mode that refuses to rewrite that file.
2.2 Lockfiles
Each tool has its own lockfile format:
- npm:
package-lock.json - Yarn Classic and modern Yarn:
yarn.lock(with different formats) - pnpm:
pnpm-lock.yaml
Lockfiles store exact versions and integrity hashes so that:
- Local development, CI pipelines, and production builds can use the same dependency graph
- The package manager can reuse work already captured during dependency resolution
Rule of thumb:
Commit your lockfile to Git for apps. For libraries, lockfile strategy can vary, but you almost always commit it for the primary repo.
In CI, use the immutable install command for your package manager:
# npm: requires package-lock.json and never rewrites it
npm ci
# Modern Yarn: fails if yarn.lock would change
yarn install --immutable
# pnpm: fails if pnpm-lock.yaml is missing or out of sync
pnpm install --frozen-lockfileThese commands turn a lockfile mismatch into a failed build instead of silently changing the dependency graph. The official documentation describes the exact behavior of npm ci (opens in a new tab), yarn install --immutable (opens in a new tab), and pnpm install --frozen-lockfile (opens in a new tab). Modern Yarn and pnpm enable similar immutable behavior automatically in many CI environments, but keeping the flags explicit makes the intent clear.
2.3 Scripts
All of them support npm run style scripts:
"scripts": {
"dev": "vite",
"build": "vite build",
"test": "vitest run",
"lint": "eslint src --ext .ts,.tsx"
}Commands:
- npm:
npm run dev - pnpm:
pnpm dev(shorthand forpnpm run dev) - Yarn:
yarn dev
All three make binaries from declared dependencies available while running scripts, so you can use tools without global installs. npm, pnpm, and Yarn's node_modules linker use a .bin directory; Yarn Plug'n'Play exposes binaries without creating node_modules/.bin.
3. npm: The Default Package Manager
3.1 What Is npm?
npm is:
- The default package manager installed with Node.js
- The name of the client (
npmCLI) - Also the name of the registry (
https://registry.npmjs.org)
If you install Node, you get npm automatically:
node -v
npm -v3.2 Basic npm Workflow
Initialize a project:
npm init -yInstall dependencies:
# Add a regular dependency
npm install react react-dom
# Add a dev dependency
npm install --save-dev typescript viteThis will:
- Update
dependenciesordevDependenciesinpackage.json - Create or update
package-lock.json - Populate
node_modules/with installed packages
Run scripts:
npm run dev
npm run build
npm testUse npx (or npm exec) for one-off commands:
npx create-next-app my-app
npx vitest3.3 Pros of npm
- Comes with Node; zero extra setup
- The most widely assumed baseline across the Node.js ecosystem
- Actively improved; performance and features have gotten much better in recent versions
- npm workspaces are now a built-in way to manage monorepos
Example of npm workspaces:
{
"name": "my-monorepo",
"private": true,
"workspaces": ["apps/*", "packages/*"]
}Then:
npm install
npm run build --workspace apps/web3.4 Cons / Limitations
Compared with alternatives, npm's main trade-off is its conventional, hoisted node_modules layout. It is broadly compatible, but repeated projects can consume more disk space than pnpm's shared content-addressable store or Yarn Plug'n'Play.
Install performance is workload-dependent. Recent npm versions can be competitive, so benchmark your actual repository before switching package managers for speed alone.
4. Yarn: Classic Workflows and Modern Plug'n'Play
4.1 Why Yarn Was Created
Yarn was originally created by Facebook (and others) to address issues they saw with npm at the time:
- Slow and sometimes unreliable installs
- Non-deterministic dependency resolution
- Problems working on very large codebases
Yarn introduced:
- A different lockfile format (
yarn.lock) - Parallelized downloads and caching for speed
- A focus on deterministic installs
4.2 Yarn Classic vs Modern Yarn
There are two “families” of Yarn:
-
Yarn Classic (v1)
- Very popular, especially in older projects
- Uses
node_moduleslike npm - Great support for workspaces
-
Modern Yarn (v2+), sometimes called Berry
- Completely redesigned
- Uses Plug'n'Play (PnP) by default for new projects
- Can instead use a traditional
node_moduleslinker - Adds constraints, plugins, zero-installs, and stricter dependency checks
They share familiar commands, but they are not interchangeable. Check a repository's packageManager field and Yarn configuration before copying commands between Classic and modern projects.
4.3 Basic Yarn Workflow
Initialize a modern Yarn project:
yarn initYarn Classic also supports yarn init -y for accepting defaults. Modern Yarn's yarn init documentation (opens in a new tab) uses flags such as -p for a private package and -w for a workspace root instead.
Install dependencies:
# Add a regular dependency
yarn add react react-dom
# Add a dev dependency
yarn add --dev typescript viteRun scripts:
yarn dev
yarn build
yarn testInstall all dependencies (after cloning a repo):
yarn install4.4 Yarn Workspaces
Yarn popularized the concept of workspaces for monorepos.
In your root package.json:
{
"name": "my-monorepo",
"private": true,
"workspaces": ["apps/*", "packages/*"]
}Now:
apps/webcan depend on@my-scope/uifrompackages/ui- Yarn links them locally (no publishing required)
yarn installdeduplicates dependencies across all workspaces
Workspaces are extremely useful for:
- Shared components (
packages/ui) - Utility libraries (
packages/utils) - Config packages (
packages/eslint-config,packages/tsconfig)
4.5 Plug'n'Play in Modern Yarn
Plug'n'Play changes how modules are located:
- No
node_modulesfolder - Yarn generates a
.pnp.cjsloader containing the dependency map - Cached packages are commonly stored as one zip archive per package
- The loader resolves imports directly to those package locations instead of crawling
node_modules
Pros:
- Less filesystem work during installation
- Strong protection against undeclared, or “ghost,” dependencies
- A dependency cache that can be shared across projects or committed for zero-installs
Cons:
- Some tools and IDE integrations need PnP-aware configuration
- React Native and Expo require a
node_moduleslinker - Slightly steeper learning curve
Modern Yarn can opt back into a conventional layout with nodeLinker: node-modules in .yarnrc.yml. Yarn's Plug'n'Play guide (opens in a new tab) explains both the loader model and the compatibility trade-offs.
5. pnpm: Performance and Disk Efficiency
5.1 What Makes pnpm Different?
pnpm was created to solve two big problems:
- Repeated copies of the same package files consuming disk space
- Repeated work when the same packages are installed across projects
Its core idea:
Use a content-addressable store, hard links, and symlinks instead of copying every package into every project.
In plain English:
- pnpm keeps package content in a shared store
- Package files are hard-linked or reflinked into each project's virtual store
- Symlinks then construct the dependency graph exposed through
node_modules - Projects using identical package content can reuse the same files on disk
5.2 Basic pnpm Workflow
Install pnpm (one-time):
npm install -g pnpmInitialize:
pnpm initInstall dependencies:
# Regular dependency
pnpm add react react-dom
# Dev dependency
pnpm add -D typescript viteRun scripts:
pnpm dev
pnpm build
pnpm testInstall all dependencies:
pnpm install5.3 pnpm’s Node Modules Layout
pnpm creates a special node_modules structure:
- Package files appear under a virtual store, normally
node_modules/.pnpm - Those files are linked from pnpm's content-addressable store
- Symlinks connect dependencies to one another and expose direct dependencies at the project root
- The layout remains compatible with Node's module resolution algorithm
Benefits:
- Massive disk space savings, especially in monorepos
- Faster installs once packages are cached
- Helps catch bad patterns like accessing undeclared dependencies
The exact global store location is platform- and configuration-dependent; use pnpm store path rather than assuming a path such as ~/.pnpm-store. The pnpm layout documentation (opens in a new tab) walks through the hard links and symlinks step by step.
5.4 pnpm Workspaces
pnpm has first-class support for workspaces via pnpm-workspace.yaml:
packages:
- 'apps/*'
- 'packages/*'Then:
pnpm install
pnpm -r test # run test in all packages (recursive)
pnpm -r buildThe -r (recursive) flag is extremely handy in monorepos.
5.5 Why Many Monorepos Prefer pnpm
You’ll often see pnpm used in larger monorepos because:
- It uses far less disk space
- Workspace support is solid and simple
- It enforces better dependency hygiene
- It keeps install times very competitive
For a big repo with many packages and branches, these benefits add up quickly.
6. Comparing npm, pnpm, and Yarn
Here’s a simplified overview:
| Feature / Aspect | npm | Yarn | pnpm |
|---|---|---|---|
| Default with Node.js | Yes | No | No |
| Lockfile | package-lock.json | yarn.lock | pnpm-lock.yaml |
| Default install model | Hoisted node_modules | Classic: node_modules; modern: PnP | Linked node_modules with a virtual store |
| Cross-project disk reuse | Download cache; project files are installed separately | Strong with PnP and its cache | Strong through the content-addressable store |
| Workspaces | Built in | Built in | Built in |
| Immutable CI install | npm ci | yarn install --immutable | pnpm install --frozen-lockfile |
No node_modules option | No | Yes, with modern Yarn PnP | No |
| Adoption effort | Usually lowest | Higher when adopting modern Yarn or PnP | Moderate; tooling must tolerate the linked layout |
Cold and warm install speed can change with the dependency graph, cache state, filesystem, network, lifecycle scripts, and package-manager version. Treat published benchmarks as starting points, then measure the commands your repository actually runs in local development and CI.
All three are good choices in 2026. The question is more about fit than “which one is objectively best.”
7. Choosing the Right Package Manager
Here are some practical guidelines.
7.1 When npm Is a Great Choice
Use npm if:
- You prefer simplicity and minimal tooling decisions
- You’re working on small to medium apps
- Your team is new to Node/JS tooling
- You want to avoid explaining extra global tools
Recent npm versions support:
- Workspaces
- Lockfiles
- Good performance
For many apps, npm is more than enough.
7.2 When Yarn Makes Sense
Use Yarn if:
- Your team is familiar with Yarn from past projects
- You want workspaces, and your organization already standardized on Yarn
- You want modern Yarn's constraints, plugins, zero-installs, or PnP dependency checks
- You have verified that your framework, IDE, and deployment tooling support your chosen linker
For an existing Yarn repository, staying on its current major version may be less risky than migrating package managers and upgrading Yarn at the same time.
7.3 When pnpm Shines
Use pnpm if:
- You’re building a monorepo with many packages
- Disk usage and install performance matter a lot (e.g., CI, many branches)
- You want stricter dependency resolution to catch bad imports
- You like the idea of a shared, content-addressable store
pnpm is especially attractive for:
- Design system repos (
packages/ui,packages/tokens, etc.) - Backend + frontend apps sharing common utilities
- Multi-app setups with shared libraries
8. Migrating Between Package Managers
You can switch package managers, but a migration changes more than the command developers type. Different resolution and linking models can expose peer-dependency conflicts, undeclared dependencies, or tool assumptions.
Use a migration sequence that preserves a rollback point:
- Start from a clean branch with the current lockfile committed
- Pin the target tool and version in the root
package.json - Import the existing lockfile when the target package manager supports it
- Remove or move generated install artifacts such as
node_modulesso the next install uses the target tool's layout - Install dependencies and inspect the new lockfile
- Run tests, builds, local development, and the immutable CI install
- Remove the old lockfile and obsolete tool-specific configuration only after validation
For example, pnpm can generate pnpm-lock.yaml from package-lock.json or yarn.lock:
pnpm import
pnpm install --frozen-lockfile
pnpm test
pnpm buildThe pnpm import command (opens in a new tab) preserves more of the existing resolution intent than deleting the old lockfile and resolving everything from scratch. Conversion is not a guarantee of an identical graph, so review the diff and test before removing the source lockfile.
After migrating:
- Pick one per repo
- Pin it with a
packageManagerfield such as"packageManager": "pnpm@<version>" - Document it in your README
- Update contributor setup, CI caches, deployment configuration, and dependency automation
- Avoid mixing package managers in the same working tree
9. Summary
npm, pnpm, and Yarn all solve the same core problem: managing JavaScript dependencies. But they do it with different trade-offs:
- npm: simple, stable, and ships with Node. Great default for many apps.
- Yarn: established deterministic workflows and workspaces, while modern Yarn adds PnP, constraints, plugins, and zero-installs.
- pnpm: aggressively efficient with disk and installs, ideal for monorepos and large codebases.
If you’re starting a new project today:
- For a single app or small project: npm or pnpm
- For a monorepo with multiple apps and packages: pnpm or Yarn workspaces
- For teams already standardized on something: stick with what your team knows, unless you have a strong reason to switch
Ultimately, the “best” package manager is the one your team understands and uses consistently, backed by good practices:
- Committed lockfiles
- A pinned package-manager version
- Immutable installs in CI
- Clear scripts
- Consistent workflow in local dev and CI
Pick one, learn it well, and let it disappear into the background while you focus on building great products.
Vikram Dokkupalle
Frontend Engineer & UI/UX Enthusiast. Passionate about React, performance, and clean design.



