Understanding pnpm: Solving Dependency Management Issues in Modern Frontend Development
This article explains the evolution of JavaScript package managers, the shortcomings of npm and Yarn such as duplicated installations, phantom dependencies and unpredictable dependency trees, and demonstrates how pnpm’s content‑addressable store, hard‑link and symlink strategy provides faster installs, reduced disk usage, and more reliable dependency isolation for frontend projects.
Efficient package management and dependency governance are crucial for modern frontend development. As projects grow, traditional tools like npm and yarn encounter problems such as duplicate installations, wasted disk space, and version conflicts.
This article explores the core features of pnpm (performant npm) and its practical applications, covering:
The development history of package managers and the unique advantages of pnpm .
The root causes of "phantom dependencies".
The uncertainty of dependency structures.
The working principles of pnpm compared with npm and yarn .
Best practices for dependency governance using pnpm .
npm’s Birth and Evolution
In the early web era, JavaScript code was managed manually. With the 2009 release of Node.js, the need for modular development grew, leading Isaac Z. Schlueter to create npm in 2010. npm introduced the node_modules directory, enabling each project to maintain its own dependencies and avoid version clashes.
npm quickly became the largest software registry, hosting over 2 million packages and handling billions of weekly downloads. However, as projects scaled, npm showed several inherent issues:
node_modules directory bloat due to nested and duplicated dependencies. Slow installation because of repeated downloads and disk writes. Complex dependency structures caused by flattening algorithms. Significant disk space waste from storing identical packages across projects.
These drawbacks spurred the creation of new tools like Yarn (2016) and pnpm (2017).
Problems with Nested Dependency Models
Before npm 3, each package installed its own dependencies, forming deep nested trees. Example structure:
node_modules
└─ DepA
├─ index.js
├─ package.json
└─ node_modules
└─ DepB
├─ index.js
├─ package.json
└─ node_modules
└─ DepC ...While this avoided version conflicts, it caused:
Excessive disk usage due to duplicated packages.
Path length limits on Windows (260‑character limit).
Slow install and update times.
npm 3 Flattening and Yarn
npm 3 introduced a flattening strategy that reduced duplication by moving shared dependencies to the top‑level node_modules . Yarn later added deterministic installs via yarn.lock , parallel downloads, offline mode, and a better CLI, but it still relied on a flattened node_modules layout, leading to repeated storage of identical packages and lingering phantom dependencies.
pnpm’s Innovative Approach
pnpm stores every downloaded package in a global content‑addressable store and creates hard links to that store for each project. Its key characteristics:
Extremely fast installation through parallel resolve‑fetch‑write stages.
High disk‑space efficiency via hard‑link sharing.
Symlink‑based virtual store ( .pnpm ) that isolates dependencies while preserving a flat appearance.
Installation flow comparison:
npm / yarn – resolve → fetch → write (sequential per package).
pnpm – resolve, fetch, and write are performed in parallel, followed by a linking phase that assembles the final node_modules structure.
Hard Links and Soft Links
pnpm’s global .pnpm-store holds the actual package files. Projects contain a .pnpm directory with symlinks pointing to the store. The top‑level node_modules contains only direct dependencies, each represented by a symlink to its location inside .pnpm . This eliminates duplicate copies and reduces install time.
node_modules
└── A // symlink to .pnpm/[email protected]/node_modules/A
└── B // symlink to .pnpm/[email protected]/node_modules/B
└── .pnpm
├── [email protected]
│ └── node_modules
│ └── A →
/A
└── [email protected]
└── node_modules
└── B →
/BPhantom (Ghost) Dependencies
When a package’s transitive dependencies are flattened, a project can import a module that is not declared in its own package.json . For example, installing express brings in body-parser , which becomes directly reachable in node_modules . If express later removes body-parser , the project may break unexpectedly.
Uncertainty in Dependency Trees
Flattening can produce different node_modules layouts depending on the order of dependencies in package.json . This nondeterminism is why lock files ( package-lock.json , yarn.lock ) were introduced to guarantee a consistent tree after installation.
pnpm’s Dependency Governance Practices
To manage redundant and overlapping dependencies, especially in monorepos, pnpm recommends:
Running pnpm why <package> to trace why a package is present.
Searching the codebase for unused imports.
Removing suspicious packages and reinstalling with pnpm i to verify build integrity.
Centralising shared dev‑dependencies at the root package.json and using resolutions to enforce consistent versions.
Leveraging peerDependencies for libraries that need to be consumed by multiple sub‑projects.
Example script (written in ES modules) scans monorepo sub‑projects for overlapping dependencies and highlights version mismatches using chalk for colourised output.
import fs from 'fs';
import path from 'path';
import { fileURLToPath } from 'url';
import chalk from 'chalk';
const __dirname = path.dirname(fileURLToPath(import.meta.url));
function readPackageJson(filePath) {
try {
const jsonData = fs.readFileSync(filePath, 'utf8');
return JSON.parse(jsonData);
} catch (error) {
console.error(`Failed to read: ${filePath}`, error);
return null;
}
}
function compareDependencies(rootDeps, childDeps, depType, childName) {
const overlaps = [];
for (const [dep, version] of Object.entries(childDeps)) {
if (rootDeps[dep]) {
const same = rootDeps[dep] === version;
overlaps.push(`${dep}: ${chalk.blueBright(version)} (root: ${chalk.blueBright(rootDeps[dep])}) ${same ? chalk.green('✔') : chalk.red('✘')}`);
}
}
return { overlaps: overlaps.length ? `${chalk.greenBright('- Overlap', depType)}\n` + overlaps.join('\n') + '\n\n' : '' };
}
function main() {
const rootPkgPath = path.join(__dirname, 'package.json');
const rootPkg = readPackageJson(rootPkgPath);
if (!rootPkg) return;
console.log(chalk.bold('📖 Dependency Analysis Report\n'));
const packagesDir = path.join(__dirname, 'packages');
const childDirs = fs.readdirSync(packagesDir).filter(d => fs.statSync(path.join(packagesDir, d)).isDirectory());
for (const child of childDirs) {
const childPkgPath = path.join(packagesDir, child, 'package.json');
const childPkg = readPackageJson(childPkgPath);
if (childPkg) {
console.log(chalk.bold(`🟢 Sub‑project ${child}`));
['dependencies', 'devDependencies', 'peerDependencies'].forEach(type => {
const { overlaps } = compareDependencies(rootPkg[type] || {}, childPkg[type] || {}, type, child);
console.log(overlaps);
});
}
}
}
main();Conclusion
pnpm offers clear advantages—speed, disk‑space efficiency, and deterministic installs—making it a compelling choice for modern frontend projects despite its still‑maturing ecosystem. Adopting pnpm, combined with disciplined dependency governance, can significantly improve project health and developer productivity.
Rare Earth Juejin Tech Community
Juejin, a tech community that helps developers grow.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.