I'm rewriting npm install because it's slow. Not slow in the abstract, but slow in a specific way: it spends 80% of its wall time on filesystem syscalls it doesn't need to make. So I built one that doesn't.
The problem with npm
Run strace npm install in any reasonably sized project. You'll see 240,000 open syscalls and 1.4M read calls. Most of them are reading metadata for packages it's already decided to install.
That's not laziness. That's how the resolver was designed: it reads package.json, then reads node_modules, then re-reads the dependency tree, then re-reads node_modules. Every byte of node_modules is touched at least three times.
The fastest tool is the one that doesn't do the work you didn't ask for.
What a minimal install looks like
My rewrite, tinypm, makes one pass:
// 200 lines · Rust 1.78 · single allocator pub async fn install(root: &Path) -> Result<()> { let manifest = read_manifest(root).await?; let tree = resolve_parallel(&manifest).await?; fetch_and_link(&tree, root).await?; Ok(()) }
Three steps. Two of them are parallelizable. resolve_parallel processes 240 packages in 800ms on my M3.
Where the perf comes from
- Lockfile parsed once, kept in memory as a graph
- Tarballs fetched concurrently · max 32 in flight
node_moduleswritten viasymlink()notcopy()when the cache has it- No
postinstallscripts unless explicitly opted in
Result on a fresh checkout of this repo: 1.4 seconds vs npm's 38. Same tree. Same modules.
Try tinypm
The 0.6 release is up. Binary for macOS, Linux, Windows. ~600KB, no runtime.
$ curl tinypm.sh | sh →What I broke (and what I fixed)
Initially I assumed every package's postinstall was malicious. That broke six packages I depend on. So I added an explicit allowlist. Surface area: 14 lines. Confusion budget: zero.
Still working on registry mirrors, peer-dep warnings that aren't garbage, and a watch mode. Issues + PRs at github.com/leekim/tinypm.