npm and yarn are both widely used package managers in the JavaScript ecosystem, responsible for installing, managing, and resolving dependencies in frontend and Node.js projects. npm ships with Node.js by default and has evolved significantly over the years to include features like workspaces, deterministic installs, and improved performance. yarn, originally developed by Meta (formerly Facebook), was created to address early shortcomings in npm—particularly around speed, determinism, and offline support—and introduced innovations like Plug’n’Play (PnP) and zero-installs. Both tools now support modern workflows including monorepos, lockfiles, and script execution, but they differ in architecture, philosophy, and advanced capabilities.
Both npm and yarn solve the same core problem — managing JavaScript dependencies — but they take different paths in how they install packages, resolve versions, handle caching, and structure project workflows. For teams making architectural decisions, understanding these differences goes beyond preference; it affects build reliability, onboarding speed, and long-term maintainability.
npm uses package-lock.json to lock dependency versions. Since v5, it guarantees reproducible installs by default — meaning two developers running npm install on the same package.json will get identical dependency trees, assuming the same npm version.
// package-lock.json (npm)
{
"name": "my-app",
"version": "1.0.0",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": { "dependencies": { "lodash": "^4.17.21" } },
"node_modules/lodash": { "version": "4.17.21", "resolved": "..." }
}
}
yarn uses yarn.lock, which takes a slightly different approach: it records every possible resolution path for a given set of constraints, not just the one chosen. This makes it more resilient when minor version ranges shift.
# yarn.lock (Yarn Classic or Berry)
lodash@^4.17.21:
version "4.17.21"
resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564f30a7478e0555d3e1b0fe4719a4004"
integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==
Key Insight: While both produce deterministic installs today,
yarn.lockis more explicit about transitive dependency versions, which can reduce “works on my machine” issues in complex dependency graphs.
npm uses a global cache and installs packages sequentially by default (though recent versions use some parallelization). The local node_modules folder is always created, which can be slow on Windows or in Docker layers due to file system overhead.
# npm install — creates node_modules
npm install
yarn (especially Yarn Berry, v2+) introduces Plug’n’Play (PnP), which eliminates node_modules entirely. Instead, it generates a .pnp.cjs file that maps every package to its exact location in the global cache. This drastically reduces disk I/O and speeds up installs.
# yarn install with PnP — no node_modules
yarn install
# Generates .yarn/cache and .pnp.cjs
Real-World Impact: In monorepos with hundreds of packages, Yarn’s PnP can cut install times from minutes to seconds and shrink Docker image sizes by avoiding redundant copies of dependencies.
Both support workspaces, but their implementations differ.
npm added workspace support in v7. You define them in package.json:
{
"workspaces": ["packages/*"]
}
Running npm install hoists shared dependencies to the root node_modules. Cross-package linking works via npm link or automatic aliasing.
yarn has had mature workspace support since v1. In Yarn Berry, workspaces are first-class citizens:
# .yarnrc.yml
nodeLinker: pnp
# package.json
{
"workspaces": ["packages/*"]
}
Yarn automatically links internal packages without symlinks, and with PnP, resolution is instant because everything is pre-mapped.
Architectural Note: If your team uses a monorepo with tight inter-package coupling (e.g., shared UI components and utilities), Yarn’s workspace + PnP combo offers more predictable and faster local development.
npm is intentionally minimal. It doesn’t support plugins or custom resolvers. All logic is baked into the CLI.
yarn (Berry) is built as a plugin system. You can add custom resolvers (e.g., for private protocols like github: or patch:), inject lint rules during install, or even override how scripts run.
# .yarnrc.yml
plugins:
- path: .yarn/plugins/@yarnpkg/plugin-interactive-tools.cjs
spec: "@yarnpkg/plugin-interactive-tools"
# Use patch protocol to fix a bug in a dep without forking
"react-error-overlay@npm:^6.0.9":
version: 6.0.9
resolution: "react-error-overlay@patch:react-error-overlay@npm%3A6.0.9#./patches/react-error-overlay.patch::version=6.0.9&hash=xxx"
When It Matters: Need to hotfix a third-party package temporarily? Yarn’s
patch:protocol lets you do it cleanly without publishing a fork.
Both support scripts in package.json and commands like run, test, and build.
However, yarn offers richer built-in utilities:
yarn dlx — run a package without installing it (like npx, but with better caching)yarn why — explains why a package is installed (more detailed than npm explain)yarn set version — switch between Yarn releases easily# Run a one-off tool with persistent caching
yarn dlx create-react-app my-app
npm keeps things simple: npx exists, but caching is less aggressive, and diagnostic tools are more basic.
This is where yarn shines uniquely. With zero-installs, you commit the entire cache (/.yarn/cache) and the PnP map to Git. New developers (or CI runners) skip yarn install entirely — dependencies are already present and mapped.
# After cloning repo with zero-installs enabled
yarn run dev # Works immediately — no install step!
npm has no equivalent. Every environment must run npm install, even if dependencies haven’t changed.
Trade-off: Zero-installs bloat your Git repo (though
.yarn/cachecan be compressed), so it’s best for teams prioritizing onboarding speed over repo size.
Despite differences, both tools converge on key modern practices:
integrity fields).npm audit, yarn audit) check for known vulnerabilities..npmrc or .yarnrc.yml.# .npmrc
registry=https://npm.pkg.github.com/my-org
//npm.pkg.github.com/:_authToken=${GITHUB_TOKEN}
# .yarnrc.yml
npmRegistryServer: "https://npm.pkg.github.com/my-org"
npmAuthToken: "${GITHUB_TOKEN}"
preinstall, postinstall, etc., and pass environment variables reliably.| Feature | npm | yarn |
|---|---|---|
| Default with Node | ✅ Yes | ❌ No |
| Lockfile | package-lock.json | yarn.lock |
| node_modules | Always created | Optional (PnP avoids it) |
| Monorepo Workspaces | Supported (v7+) | Mature, deeply integrated |
| Zero-Installs | ❌ Not possible | ✅ Fully supported |
| Extensibility | Minimal | Plugin-based architecture |
| Offline Mode | Limited (relies on cache) | Robust (with cache committed) |
| Learning Curve | Low | Moderate (especially with PnP) |
Stick with npm if your team values minimalism, avoids extra configuration, and works on standard apps where node_modules overhead isn’t a bottleneck. It’s the safe, universal choice.
Adopt yarn if you’re building large-scale applications or monorepos where install speed, disk efficiency, and advanced dependency control directly impact developer productivity and CI costs. Be prepared to invest in understanding PnP and its ecosystem implications (e.g., some tools may need PnP compatibility patches).
Neither is “better” universally — but for frontend architects, the right choice aligns with your team’s scale, performance constraints, and appetite for adopting opinionated tooling.
Choose npm if you prefer a stable, built-in tool that integrates seamlessly with Node.js and requires minimal setup. It’s ideal for teams that value simplicity, broad compatibility, and avoiding additional tooling layers—especially in environments where consistency with default Node.js behavior is critical. Recent versions have closed many historical gaps in performance and determinism, making it sufficient for most standard applications.
Choose yarn if you need advanced dependency resolution strategies like Plug’n’Play (PnP) to eliminate node_modules, or if your team benefits from features like zero-installs for faster CI/CD and onboarding. It’s well-suited for large monorepos or performance-sensitive workflows where strict control over module resolution and caching behavior is required, and you’re willing to adopt its specific conventions and tooling model.
You should be running a currently supported version of Node.js to run npm. For a list of which versions of Node.js are currently supported, please see the Node.js releases page.
npm comes bundled with node, & most third-party distributions, by default. Officially supported downloads/distributions can be found at: nodejs.org/en/download
You can download & install npm directly from npmjs.com using our custom install.sh script:
curl -qL https://www.npmjs.com/install.sh | sh
If you're looking to manage multiple versions of Node.js &/or npm, consider using a node version manager
npm <command>
npm help-search <query>npm is configured to use the npm Public Registry at https://registry.npmjs.org by default; Usage of this registry is subject to Terms of Use available at https://npmjs.com/policies/termsnpm to use any other compatible registry you prefer. You can read more about configuring third-party registriesnpm should never be capitalized unless it is being displayed in a location that is customarily all-capitals (ex. titles on man pages).
Contrary to popular belief, npm is not in fact an acronym for "Node Package Manager"; It is a recursive bacronymic abbreviation for "npm is not an acronym" (if the project was named "ninaa", then it would be an acronym). The precursor to npm was actually a bash utility named "pm", which was the shortform name of "pkgmakeinst" - a bash function that installed various things on various platforms. If npm were to ever have been considered an acronym, it would be as "node pm" or, potentially "new pm".