Summary
In the fresh-resolve install path (no --frozen-lockfile, no usable lockfile), pacquet's dep-tree walker fetches package metadata serially as it recurses. pnpm pipelines/batches the same fetches, so by the time the walker reaches a node its packument is already in memory. The vlt.sh clean / cache / node_modules / lockfile variations exercise this path and pacquet is 4-12× slower than pnpm on deep-tree fixtures.
Benchmark evidence
From benchmarks.vlt.sh/latest/chart-data.json — selected rows where the lockfile is wiped or stale (pacquet 0.2.8 vs pnpm 11.2.2):
| Variation |
Fixture |
pnpm |
pacquet |
ratio |
clean |
astro |
2.87s |
18.33s |
6.4× slower |
cache |
astro |
1.48s |
11.22s |
7.6× slower |
cache+node_modules |
astro |
2.42s |
9.31s |
3.8× slower |
node_modules |
astro |
3.41s |
16.56s |
4.9× slower |
lockfile |
astro |
2.79s |
13.17s |
4.7× slower |
cache+lockfile |
astro |
0.91s |
4.03s |
4.4× slower |
astro is a single astro@^5 dep that pulls hundreds of transitive packages — the worst case for a serial walker.
Current pacquet behavior
pacquet/crates/resolving-deps-resolver/src/resolve_dependency_tree.rs walks the dep tree with #[async_recursion]. Each resolve() for a (name, range) consults the per-(registry, name) packument cache initialized at install_with_fresh_lockfile.rs#L298-L334, but if there's no cached packument the fetch happens inline and blocks the recursion at that node.
There is no prefetcher / lookahead — descendants don't get their packuments requested until their parent's resolve() returns. With a tree depth of 10+ and fan-out of 5+, this serializes hundreds of HTTP requests that pnpm makes concurrent.
Proposed approach
Two complementary changes, both modeled on pnpm's resolver:
-
Prefetch the direct deps' packuments before starting the tree walk. Spawn n tokio tasks for the manifest's direct deps so the first recursion level has its packuments ready in cache.
-
Lookahead/batched fetch inside the recursion. When resolve() returns a manifest that lists dependencies / optionalDependencies, immediately fire packument fetches for each child name in parallel (de-duplicated via the existing per-(registry, name) mutex map). The recursion then awaits a cache hit instead of a blocking fetch.
pnpm's equivalent is in resolving/npm-resolver/src/index.ts and the prefetcher around installing/deps-resolver/src/resolveDependencyTree.ts.
This is implicitly part of #11633 Stage 2 Tier 1 ("Resolution graph & recursion engine") but worth tracking as a focused perf sub-task with the vlt.sh benchmark numbers attached so progress is measurable.
Expected impact
Closes the astro outlier across all lockfile-wiped variations. Smaller but meaningful gain on large and any other deep-tree resolve.
Related
Written by an agent (Claude Code, claude-opus-4-7).
Summary
In the fresh-resolve install path (no
--frozen-lockfile, no usable lockfile), pacquet's dep-tree walker fetches package metadata serially as it recurses. pnpm pipelines/batches the same fetches, so by the time the walker reaches a node its packument is already in memory. The vlt.shclean/cache/node_modules/lockfilevariations exercise this path and pacquet is 4-12× slower than pnpm on deep-tree fixtures.Benchmark evidence
From
benchmarks.vlt.sh/latest/chart-data.json— selected rows where the lockfile is wiped or stale (pacquet 0.2.8 vs pnpm 11.2.2):cleancachecache+node_modulesnode_moduleslockfilecache+lockfileastrois a singleastro@^5dep that pulls hundreds of transitive packages — the worst case for a serial walker.Current pacquet behavior
pacquet/crates/resolving-deps-resolver/src/resolve_dependency_tree.rswalks the dep tree with#[async_recursion]. Eachresolve()for a(name, range)consults the per-(registry, name)packument cache initialized atinstall_with_fresh_lockfile.rs#L298-L334, but if there's no cached packument the fetch happens inline and blocks the recursion at that node.There is no prefetcher / lookahead — descendants don't get their packuments requested until their parent's
resolve()returns. With a tree depth of 10+ and fan-out of 5+, this serializes hundreds of HTTP requests that pnpm makes concurrent.Proposed approach
Two complementary changes, both modeled on pnpm's resolver:
Prefetch the direct deps' packuments before starting the tree walk. Spawn
ntokio tasks for the manifest's direct deps so the first recursion level has its packuments ready in cache.Lookahead/batched fetch inside the recursion. When
resolve()returns a manifest that listsdependencies/optionalDependencies, immediately fire packument fetches for each child name in parallel (de-duplicated via the existing per-(registry, name)mutex map). The recursion thenawaits a cache hit instead of a blocking fetch.pnpm's equivalent is in
resolving/npm-resolver/src/index.tsand the prefetcher aroundinstalling/deps-resolver/src/resolveDependencyTree.ts.This is implicitly part of #11633 Stage 2 Tier 1 ("Resolution graph & recursion engine") but worth tracking as a focused perf sub-task with the vlt.sh benchmark numbers attached so progress is measurable.
Expected impact
Closes the
astrooutlier across all lockfile-wiped variations. Smaller but meaningful gain onlargeand any other deep-tree resolve.Related
Written by an agent (Claude Code, claude-opus-4-7).