Does pnpm Make My Nuxt Site Safer and Smaller?
I started this migration with two assumptions:
- pnpm would make my application safer.
- pnpm would make my application smaller.
Both contain some truth, but neither is accurate without context. This site is generated by Nuxt and served as static files from an Nginx container. Its dependencies exist while I develop and build the site, but they do not ship in the production image. That distinction matters.
So instead of writing the conclusion first, I migrated the project, measured the same application before and after, and looked at what actually changed.
What I changed
The migration itself was deliberately narrow. I kept the existing direct dependency versions, replaced package-lock.json with pnpm-lock.yaml, pinned pnpm 11.22.0 through the packageManager field, and updated local, CI-style, and Docker commands.
The project now installs with:
pnpm install --frozen-lockfile
The Docker build installs that exact pnpm version before restoring dependencies:
RUN npm install --global pnpm@11.22.0
COPY package.json pnpm-lock.yaml pnpm-workspace.yaml ./
RUN pnpm install --frozen-lockfile
npm is still used there as the bootstrap tool already included in the Node image. It is no longer the project's package manager.
The measurements
I generated the same site with both package managers before adding this article. The two runs used the same application content and direct dependency versions.
| Measurement | npm | pnpm | Difference |
|---|---|---|---|
| Lockfile | 600,472 bytes | 394,286 bytes | 34.3% smaller |
Local node_modules after generation | 430,972 KiB | 400,952 KiB | 7.0% smaller |
| Generated public files | 10,898,351 bytes | 10,898,157 bytes | 194 bytes smaller |
| Production Nginx image | 34,091,502 bytes | 34,090,639 bytes | 863 bytes smaller |
Both generated sites contained 139 public files and the same 14 HTML routes. The tiny differences in generated bytes and container size are about 0.002%, which is noise rather than a meaningful deployment improvement.
The local node_modules comparison is more interesting: it dropped by about 29 MiB in this repository. pnpm's content-addressable store can save considerably more disk space across multiple projects because package contents can be shared. The store itself is not included in the per-project node_modules number above, so this is a local project measurement, not a universal promise.
I did not publish an installation-speed comparison. The npm and pnpm runs did not start with equivalent caches, so a stopwatch would have produced a precise-looking but misleading number.
Did pnpm make the project safer?
It made the dependency installation process stricter and easier to defend. It did not magically remove application vulnerabilities.
The first practical improvement appeared immediately: the lint command used eslint, but ESLint was not declared as a direct dependency. npm's flattened dependency tree happened to make a transitive copy available. pnpm exposed that hidden assumption, and I added ESLint explicitly. The dependency manifest now describes what the project actually uses.
I also enabled a small set of pnpm's supply-chain security controls:
minimumReleaseAge: 1440
minimumReleaseAgeStrict: true
blockExoticSubdeps: true
strictDepBuilds: true
allowBuilds:
better-sqlite3: true
esbuild: true
unrs-resolver: true
This configuration does three useful things:
- New package versions must be at least one day old before pnpm will install them. Strict mode makes the policy fail closed instead of silently selecting an unapproved version.
- Transitive dependencies cannot unexpectedly switch to Git repositories, tarballs, or other exotic sources.
- Dependency build scripts are denied unless the package has been reviewed and explicitly allowed.
Only better-sqlite3, esbuild, and unrs-resolver need install scripts in this dependency graph, so those are the only packages on the allowlist. A clean Linux container build confirmed that the allowlist is sufficient.
There is an honest caveat. pnpm also surfaced three transitive peer-dependency warnings inside the existing Nuxt toolchain. I kept the npm-resolved direct versions rather than hiding a framework upgrade inside a package-manager migration. Linting, static generation, and a clean container build all pass, while the warnings remain visible for a future dependency update.
That is the kind of safety improvement I can defend: stricter declarations, a reproducible frozen lockfile, a release-age policy, and a small reviewed build-script surface. pnpm is not a vulnerability scanner, and these controls do not replace dependency updates, audits, code review, or runtime hardening.
Did pnpm make the application smaller?
It depends on which application we mean.
The development installation became smaller, and the lockfile became substantially smaller. Across several Node projects, pnpm's shared store should improve the disk-space result further.
The deployed application did not become meaningfully smaller. This project uses Nuxt's static preset. The build stage contains Node, pnpm, and the entire dependency graph, but the final stage contains only Nginx, its configuration, and .output/public:
source + dependencies -> Nuxt generate -> static files -> Nginx image
Changing the package manager affects the left side of that pipeline. It does not remove JavaScript, images, fonts, or CSS from the already generated site. The measurements show exactly that: a smaller local installation, but an effectively identical production artifact.
What if this were a server-rendered application?
The answer could be different for a Nuxt server that deploys Node and production dependencies. In that setup, dependency layout and pruning can affect the runtime layer, build cache, transfer size, and cold starts.
Even then, pnpm alone does not guarantee a smaller server image. The result depends on whether the deployment copies a complete node_modules, uses a pruned production installation, or ships a bundled Nitro server. A fair comparison would measure the final image and runtime behavior, just as I measured the static image here.
pnpm provides tools for production deployments, but container stage design still matters more than the package-manager logo.
One migration trap worth mentioning
Do not use pnpm install --force for a size benchmark. According to the pnpm install documentation, --force installs optional dependencies even when they do not match the current operating system or architecture. I tried it while validating the migration and temporarily produced a much larger installation. A normal frozen install is the relevant comparison.
The conclusion
Switching to pnpm was worthwhile, just not for the simplistic reasons I started with.
The project is safer because its dependency contract is stricter and its install-time attack surface is explicitly controlled. It is smaller on a developer machine, especially when the shared store is useful across projects. But this static site's public output and production container are essentially unchanged.
My final version of the original claim is therefore:
pnpm makes this project's dependency workflow stricter and its local installation smaller. It does not make the statically deployed website meaningfully smaller.
That is less dramatic than “safer and smaller,” but it is also measurable, reproducible, and true.