npm, pnpm or Bun: Choosing a Package Manager in 2026
The three serious options for installing JavaScript dependencies now differ less on speed than on what they refuse to do. We compared npm, pnpm and Bun on disk use, lockfiles, monorepos and the supply-chain defaults that matter after a year of poisoned packages, and on the one change to Node itself that affects all three.

Quick answer
For a new project we would pick pnpm. Its strict node_modules layout stops code importing packages it never declared, its content-addressable store means one copy of each package on disk however many projects use it, and since version 11 it waits 24 hours before installing any newly published version — which is the single most effective defence against the malicious releases that hit npm through 2025. Bun's installer is the fastest of the three and works in ordinary Node projects, but its speed matters most in CI and least on a laptop. npm is fine if you are on it already and have turned lifecycle scripts off; it is the one of the three that still runs a dependency's install scripts by default.
For most of the last decade the package manager question was about speed, and the answer changed every eighteen months. That argument is mostly over. All three serious options are fast enough on a warm cache that you will not notice the difference on a laptop, and where it does matter, in CI, the fix is usually caching rather than switching tools.
What separates them now is what they refuse to do. After a year in which the npm registry was hit by a run of compromised packages, some of them self-propagating, the defaults around install scripts and freshly published versions are the reason to choose one over another. We run projects on all three. Here is how we decide.
The short version
| npm | pnpm | Bun | |
|---|---|---|---|
| Ships with Node | Yes | No | No |
| Disk use across projects | One copy per project | One copy per machine, hard-linked | Global cache, copied or linked per project |
| node_modules layout | Flat (hoisted) | Strict, symlinked | Flat (hoisted) |
| Dependency install scripts | Run by default | Blocked until allowed | Blocked until trusted |
| Delay on new versions | None | 24 hours by default (v11) | None |
| Lockfile | package-lock.json | pnpm-lock.yaml | bun.lock (text, since 1.2) |
| Workspaces | Yes | Yes | Yes |
npm: fine, if you change two defaults
npm's case is that it is already there. It comes with Node, every tutorial assumes it, and npm ci does the one thing a CI install must do: install exactly what the lockfile says and fail loudly if package.json has drifted from it. If your project is small and your team is not fighting it, there is no prize for switching.
The two things we change on every npm project are both about scripts. First, ignore-scripts=true in .npmrc, so a dependency's postinstall cannot run code on your machine just because you typed install. The handful of packages that genuinely need a build step, native modules mostly, you then run by hand or list explicitly. Second, npm ci rather than npm install anywhere automated, which we covered in more detail in our piece on keeping CI under ten minutes.
What npm does not give you is any protection against a package that was fine yesterday and compromised this morning. If a version is on the registry, npm will install it.
pnpm: the defaults we would want everywhere
pnpm started as a disk-space fix and became the safest of the three almost as a side effect of being strict. The disk-space part still holds: every package version lives once in a content-addressable store on your machine, and projects hard-link to it. Its documentation puts it simply — when packages are installed, their files are hard-linked from that single place, consuming no additional disk space. Ten projects on one laptop that all use the same React means one React on disk.
The strictness is the part that changes how you write code. pnpm puts only your declared dependencies at the top of node_modules, so import lodash from "lodash" fails unless lodash is actually in your package.json, rather than quietly working because something else pulled it in. That is the phantom dependency problem, and it is the cause of a whole class of "works on my machine, breaks in production" bugs. If some tool you rely on cannot cope with the symlinked layout, nodeLinker: hoisted gives you a flat tree and you lose only that check.
Since version 11, pnpm waits 24 hours before it will install a newly published version of anything, including transitive dependencies. Most malicious releases are pulled from the registry within an hour. That one default is worth more than any audit tool we have used.
Two settings do the security work. minimumReleaseAge defines the minimum number of minutes that must pass after a version is published before pnpm will install it, and defaults to 1440 in v11. And since v10.3, dependency build scripts do not run at all unless you allow the package by name; with strictDepBuilds on, which is the default, an install with unreviewed scripts fails rather than silently skipping them. You will spend five minutes approving esbuild and sharp on a new project. That is the cost.
Bun: the fastest installer, in a runtime you may not want
Bun's package manager is the one people mean when they say "Bun is fast". Bun's own page claims installs many times faster than npm; we would treat the headline multiplier as a best case on a cold cache, but the direction is right. On a CI runner with nothing cached, bun install --frozen-lockfile is consistently the quickest way we know to get from a checkout to a node_modules.
The point that is easy to miss is that you do not have to run your app on Bun to use it. The documentation is explicit: it is a standalone tool that works in existing Node.js projects; if your project has a package.json, you can use bun install. So the practical pattern is Bun for the install step and Node for everything else, which is what we do on a couple of pipelines where installing was the slow part.
On safety, Bun is closer to pnpm than to npm. It does not execute lifecycle scripts like postinstall for installed dependencies unless you list the package in trustedDependencies. What it lacks is any equivalent of pnpm's release-age delay. And the lockfile, binary until 1.2, is now plain text and reviewable in a pull request, which removes the objection we used to have.
The Corepack change affects all three
Corepack was the tool that made the packageManager field in package.json mean something: with it installed, typing pnpm ran the exact version your project pinned, downloading it if needed. Node distributed it from 14.19 up to but not including 25.0.0, and from 25 it is gone. The Node steering committee voted to stop shipping it, on the argument that package managers should not be bundled with the runtime.
Nothing breaks on Node 24, which is the current LTS and still includes it. But the first person on your team to install Node 25 will find pnpm is suddenly not a command, and the fix is one line: npm install -g corepack. Put it in your setup docs and your CI image now. If you would rather not depend on Corepack at all, pnpm's own standalone installer and Bun's install script both work without it.
Which one, then
- Starting a project: pnpm. The strict layout and the 24-hour delay are the defaults we would choose if we were designing a package manager today, and workspaces are good enough that we no longer reach for a separate monorepo tool for small setups.
- Existing npm project that works: stay, set
ignore-scripts, usenpm ci. Migrate when you next touch the monorepo structure or the CI install time, not before. - Install step is the slow part of CI: Bun for the install, Node for the app. Cheap to try, easy to reverse.
- Already on Bun as a runtime: use its installer; the two are built to go together.
Whichever you pick, the same three habits apply: commit the lockfile, install with the frozen flag in CI, and do not let dependencies run scripts you have not read. The tool makes those easier or harder. It does not make them optional. And if the project is a Next.js app you are about to deploy, the package manager you choose also has to be one your host supports — our deployment comparison notes where that bites.
Pros and cons
Pros
- All three read the same package.json and npm registry, so switching is a lockfile change rather than a rewrite
- pnpm and Bun both refuse to run dependency install scripts unless you allow them by name
- pnpm's release-age delay turns most registry compromises into a non-event
Cons
- Corepack, the tool that pinned a package manager version per project, is no longer shipped with Node 25 and has to be installed separately
- pnpm's symlinked layout still trips up the occasional tool that expects a flat node_modules
- Bun's speed claims are Bun's own numbers; the gap on a warm cache is much smaller
Alternatives worth considering
Ships with Node. npm ci installs exactly what the lockfile says and fails if package.json disagrees. Runs dependency lifecycle scripts unless you pass --ignore-scripts or set it in .npmrc.
Content-addressable store with hard links, strict non-flat node_modules, workspaces. Dependency build scripts are blocked until allowed; new versions are held for 24 hours by default since v11.
Standalone installer that works in any project with a package.json. Text lockfile since 1.2, workspaces, --frozen-lockfile, and no lifecycle scripts for dependencies unless listed in trustedDependencies.
Reads the packageManager field in package.json and runs the pinned version. Bundled with Node from 14.19 up to but not including 25; install it with npm install -g corepack on newer runtimes.
Frequently asked questions
Is it worth migrating an existing npm project?
If it is small and stable, probably not; turn off lifecycle scripts and carry on. If it is a monorepo, has a slow CI install, or you have been bitten by a dependency importing something it never declared, pnpm pays for the afternoon it takes. Delete node_modules and package-lock.json, run pnpm import to build the new lockfile from the old one, then pnpm install.
Does Bun's package manager mean I have to run Bun instead of Node?
No. Bun's own documentation describes the installer as a standalone tool that works in existing Node.js projects. You can use bun install in CI for the speed and keep running the app on Node. Whether that split is worth the second tool is a fair question; we mostly use it where the install step is the slow part of a pipeline.
What does the Corepack change actually break?
Nothing you have already set up on Node 24 or earlier, which still ship it. On Node 25 and later, the packageManager field in package.json does nothing until someone installs Corepack globally. If your team relies on it to keep everyone on the same pnpm version, add that install to your setup docs and your CI image now, before the first person upgrades Node.
Why hold back new versions for a day?
Because that is how long it takes to catch a bad one. pnpm's documentation puts it plainly: malicious releases are usually discovered and removed from the registry within an hour. A 24-hour minimum release age means your install never sees them. You can exempt packages you publish yourself with minimumReleaseAgeExclude.
Sources
Everything factual in this article traces back to one of these. Vendors change pricing and limits without changing the URL, so each entry records the date we last read it.
- Motivation
pnpmchecked September 14, 2026
- Settings: dependency resolution (minimumReleaseAge)
pnpmchecked September 14, 2026
- Settings: build (allowBuilds, strictDepBuilds)
pnpmchecked September 14, 2026
- bun install
Bunchecked September 14, 2026
- npm ci
npm (GitHub)checked September 14, 2026
- nodejs/corepack README
Node.js (GitHub)checked September 14, 2026
- Node.js TSC votes to stop distributing Corepack
Socketchecked September 14, 2026
Written by
ToolNest Editorial
Editorial team
ToolNest's editorial byline. Our articles summarise and compare software using vendor documentation, changelogs, pricing pages and published reporting, and are drafted with AI assistance under human review. Where we have not used a tool ourselves, we say so rather than implying otherwise.